@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,502 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
getFrappeSyncModule,
|
|
6
|
+
useFrappeCatchUp,
|
|
7
|
+
useFrappeCollection,
|
|
8
|
+
useFrappeInfiniteList,
|
|
9
|
+
useLiveQuery,
|
|
10
|
+
type SyncModule,
|
|
11
|
+
type UseFrappeCollectionConfig,
|
|
12
|
+
} from "@multiplatform.one/frappe";
|
|
13
|
+
import { FrappeProvider, useFrappeConfig } from "./FrappeProvider";
|
|
14
|
+
import { BackfillScope } from "../../frappe/src/sync/types";
|
|
15
|
+
|
|
16
|
+
const { sockets } = vi.hoisted(() => ({
|
|
17
|
+
sockets: [] as Array<{
|
|
18
|
+
connected: boolean;
|
|
19
|
+
deliver: (event: string, payload?: unknown) => void;
|
|
20
|
+
}>,
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("socket.io-client", () => ({
|
|
23
|
+
io: () => {
|
|
24
|
+
const listeners = new Map<string, Set<(payload?: unknown) => void>>();
|
|
25
|
+
const socket = {
|
|
26
|
+
id: "synthetic",
|
|
27
|
+
connected: false,
|
|
28
|
+
auth: {},
|
|
29
|
+
io: { on: vi.fn(), opts: {} },
|
|
30
|
+
on(event: string, handler: (payload?: unknown) => void) {
|
|
31
|
+
if (!listeners.has(event)) listeners.set(event, new Set());
|
|
32
|
+
listeners.get(event)!.add(handler);
|
|
33
|
+
return socket;
|
|
34
|
+
},
|
|
35
|
+
off(event: string, handler: (payload?: unknown) => void) {
|
|
36
|
+
listeners.get(event)?.delete(handler);
|
|
37
|
+
return socket;
|
|
38
|
+
},
|
|
39
|
+
emit: vi.fn(),
|
|
40
|
+
connect() {
|
|
41
|
+
socket.connected = true;
|
|
42
|
+
socket.deliver("connect");
|
|
43
|
+
return socket;
|
|
44
|
+
},
|
|
45
|
+
disconnect() {
|
|
46
|
+
socket.connected = false;
|
|
47
|
+
return socket;
|
|
48
|
+
},
|
|
49
|
+
removeAllListeners() {
|
|
50
|
+
listeners.clear();
|
|
51
|
+
return socket;
|
|
52
|
+
},
|
|
53
|
+
deliver(event: string, payload?: unknown) {
|
|
54
|
+
if (event === "connect") socket.connected = true;
|
|
55
|
+
if (event === "disconnect") socket.connected = false;
|
|
56
|
+
for (const listener of listeners.get(event) ?? []) listener(payload);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
sockets.push(socket);
|
|
60
|
+
return socket;
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
interface Task {
|
|
65
|
+
name: string;
|
|
66
|
+
doctype: string;
|
|
67
|
+
title: string;
|
|
68
|
+
modified: string;
|
|
69
|
+
}
|
|
70
|
+
const row = (name: string, title = name): Task => ({
|
|
71
|
+
name,
|
|
72
|
+
title,
|
|
73
|
+
doctype: "Task",
|
|
74
|
+
modified: "2026-09-09T00:00:00Z",
|
|
75
|
+
});
|
|
76
|
+
const modules = new Set<SyncModule>();
|
|
77
|
+
const observed = new Map<string, { sync: SyncModule; collection: unknown }>();
|
|
78
|
+
let sequence = 0;
|
|
79
|
+
function connection(): UseFrappeCollectionConfig {
|
|
80
|
+
return { baseURL: `https://public-catch-up-${++sequence}.test`, realtime: false };
|
|
81
|
+
}
|
|
82
|
+
function Probe({ label = "main" }: { label?: string }) {
|
|
83
|
+
const config = useFrappeConfig();
|
|
84
|
+
const sync = getFrappeSyncModule(config)!;
|
|
85
|
+
modules.add(sync);
|
|
86
|
+
const collection = useFrappeCollection<Task>(config, "Task", { filters: [["title", "!=", ""]] });
|
|
87
|
+
const query = useLiveQuery(
|
|
88
|
+
(q) => (collection ? q.from({ task: collection }) : null),
|
|
89
|
+
[collection],
|
|
90
|
+
);
|
|
91
|
+
const state = useFrappeCatchUp(config);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
observed.set(label, { sync, collection });
|
|
94
|
+
}, [label, sync, collection]);
|
|
95
|
+
return (
|
|
96
|
+
<section>
|
|
97
|
+
<output data-testid={`${label}-rows`}>
|
|
98
|
+
{query.data
|
|
99
|
+
?.map((task) => `${task.name}:${task.title}`)
|
|
100
|
+
.sort()
|
|
101
|
+
.join(",")}
|
|
102
|
+
</output>
|
|
103
|
+
<output data-testid={`${label}-state`}>
|
|
104
|
+
{state.refreshing ? "refreshing" : "idle"}/{state.stale ? "stale" : "current"}
|
|
105
|
+
</output>
|
|
106
|
+
{state.error ? (
|
|
107
|
+
<p role="alert">
|
|
108
|
+
{state.error instanceof Error ? state.error.message : String(state.error)}
|
|
109
|
+
</p>
|
|
110
|
+
) : null}
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => {
|
|
113
|
+
void state.retry();
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
Retry {label}
|
|
117
|
+
</button>
|
|
118
|
+
</section>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
function InfiniteProbe() {
|
|
122
|
+
const config = useFrappeConfig();
|
|
123
|
+
const sync = getFrappeSyncModule(config)!;
|
|
124
|
+
modules.add(sync);
|
|
125
|
+
const list = useFrappeInfiniteList<Task>({ config, doctype: "Task" });
|
|
126
|
+
return (
|
|
127
|
+
<output data-testid="infinite">
|
|
128
|
+
{list.error?.message ?? list.items.map((item) => item.name).join(",")}
|
|
129
|
+
</output>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function server() {
|
|
133
|
+
let rows = [row("a"), row("b")];
|
|
134
|
+
let status = 200;
|
|
135
|
+
let cdc = {
|
|
136
|
+
rows: [] as Task[],
|
|
137
|
+
deleted: [] as { name: string }[],
|
|
138
|
+
checkpoint: "next",
|
|
139
|
+
has_more: false,
|
|
140
|
+
};
|
|
141
|
+
const urls: URL[] = [];
|
|
142
|
+
let gate: Promise<Response> | undefined;
|
|
143
|
+
const fetch = vi.fn(async (input: RequestInfo | URL) => {
|
|
144
|
+
const url = new URL(
|
|
145
|
+
typeof input === "string" ? input : input instanceof URL ? input.href : input.url,
|
|
146
|
+
);
|
|
147
|
+
urls.push(url);
|
|
148
|
+
if (gate) {
|
|
149
|
+
const pending = gate;
|
|
150
|
+
gate = undefined;
|
|
151
|
+
return pending;
|
|
152
|
+
}
|
|
153
|
+
if (status !== 200) return Response.json({ message: "Service unavailable" }, { status });
|
|
154
|
+
if (url.pathname === "/api/resource/Task") return Response.json({ data: rows });
|
|
155
|
+
if (url.pathname.startsWith("/api/resource/Task/")) {
|
|
156
|
+
const doc = rows.find((item) => item.name === url.pathname.split("/").at(-1));
|
|
157
|
+
return Response.json({ data: doc }, { status: doc ? 200 : 404 });
|
|
158
|
+
}
|
|
159
|
+
if (url.pathname === "/api/method/live.live.api.backfill")
|
|
160
|
+
return Response.json({ message: cdc });
|
|
161
|
+
return Response.json({ message: "Unsupported method" }, { status: 404 });
|
|
162
|
+
});
|
|
163
|
+
vi.stubGlobal("fetch", fetch);
|
|
164
|
+
return {
|
|
165
|
+
fetch,
|
|
166
|
+
urls,
|
|
167
|
+
setRows: (value: Task[]) => {
|
|
168
|
+
rows = value;
|
|
169
|
+
},
|
|
170
|
+
setStatus: (value: number) => {
|
|
171
|
+
status = value;
|
|
172
|
+
},
|
|
173
|
+
setCdc: (value: typeof cdc) => {
|
|
174
|
+
cdc = value;
|
|
175
|
+
},
|
|
176
|
+
hold: () => {
|
|
177
|
+
let resolve!: (value: Response) => void;
|
|
178
|
+
gate = new Promise<Response>((done) => {
|
|
179
|
+
resolve = done;
|
|
180
|
+
});
|
|
181
|
+
return resolve;
|
|
182
|
+
},
|
|
183
|
+
resources: () => urls.filter((url) => url.pathname === "/api/resource/Task"),
|
|
184
|
+
methods: () => urls.filter((url) => url.pathname.startsWith("/api/method/")),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async function expectRows(value: string, label = "main") {
|
|
188
|
+
await waitFor(() => expect(screen.getByTestId(`${label}-rows`).textContent).toBe(value));
|
|
189
|
+
}
|
|
190
|
+
async function expectIdle(label = "main") {
|
|
191
|
+
await waitFor(() =>
|
|
192
|
+
expect(screen.getByTestId(`${label}-state`).textContent).toBe("idle/current"),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
sockets.length = 0;
|
|
197
|
+
observed.clear();
|
|
198
|
+
});
|
|
199
|
+
afterEach(async () => {
|
|
200
|
+
cleanup();
|
|
201
|
+
for (const sync of modules) await sync.dispose();
|
|
202
|
+
modules.clear();
|
|
203
|
+
vi.unstubAllGlobals();
|
|
204
|
+
vi.useRealTimers();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("refreshes resource-only mounted collections for focus and online without CDC/cursor requests", async () => {
|
|
208
|
+
const api = server();
|
|
209
|
+
const config = {
|
|
210
|
+
...connection(),
|
|
211
|
+
syncOptions: { catchUpStrategy: "snapshot" as const, cdcInterval: 60000 },
|
|
212
|
+
};
|
|
213
|
+
render(
|
|
214
|
+
<FrappeProvider {...config}>
|
|
215
|
+
<Probe />
|
|
216
|
+
</FrappeProvider>,
|
|
217
|
+
);
|
|
218
|
+
await expectRows("a:a,b:b");
|
|
219
|
+
const initial = observed.get("main")!;
|
|
220
|
+
api.setRows([row("a", "edited"), row("c")]);
|
|
221
|
+
fireEvent(window, new Event("focus"));
|
|
222
|
+
await expectRows("a:edited,c:c");
|
|
223
|
+
await expectIdle();
|
|
224
|
+
api.setRows([]);
|
|
225
|
+
fireEvent(window, new Event("online"));
|
|
226
|
+
await expectRows("");
|
|
227
|
+
await expectIdle();
|
|
228
|
+
expect(observed.get("main")!.collection).toBe(initial.collection);
|
|
229
|
+
expect(api.methods()).toEqual([]);
|
|
230
|
+
expect(api.resources()).toHaveLength(3);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("dispatches the enabled interval through the public snapshot provider", async () => {
|
|
234
|
+
vi.useFakeTimers();
|
|
235
|
+
const api = server();
|
|
236
|
+
render(
|
|
237
|
+
<FrappeProvider
|
|
238
|
+
{...connection()}
|
|
239
|
+
syncOptions={{ catchUpStrategy: "snapshot", cdcInterval: 1000 }}
|
|
240
|
+
>
|
|
241
|
+
<Probe />
|
|
242
|
+
</FrappeProvider>,
|
|
243
|
+
);
|
|
244
|
+
await act(async () => {
|
|
245
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
246
|
+
});
|
|
247
|
+
expect(screen.getByTestId("main-rows").textContent).toBe("a:a,b:b");
|
|
248
|
+
api.setRows([row("timer")]);
|
|
249
|
+
await act(async () => {
|
|
250
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
251
|
+
});
|
|
252
|
+
expect(screen.getByTestId("main-rows").textContent).toBe("timer:timer");
|
|
253
|
+
expect(api.resources()).toHaveLength(2);
|
|
254
|
+
expect(api.methods()).toEqual([]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("retains rows through a real HTTP 503, renders qualification, and retries without remounting", async () => {
|
|
258
|
+
const api = server();
|
|
259
|
+
const config = { ...connection(), syncOptions: { catchUpStrategy: "snapshot" as const } };
|
|
260
|
+
const view = render(
|
|
261
|
+
<FrappeProvider {...config}>
|
|
262
|
+
<Probe />
|
|
263
|
+
</FrappeProvider>,
|
|
264
|
+
);
|
|
265
|
+
await expectRows("a:a,b:b");
|
|
266
|
+
const initial = observed.get("main")!.collection;
|
|
267
|
+
const release = api.hold();
|
|
268
|
+
api.setStatus(503);
|
|
269
|
+
fireEvent(window, new Event("focus"));
|
|
270
|
+
await waitFor(() => expect(api.resources()).toHaveLength(2));
|
|
271
|
+
expect(screen.getByTestId("main-state").textContent).toBe("refreshing/current");
|
|
272
|
+
expect(screen.getByTestId("main-rows").textContent).toBe("a:a,b:b");
|
|
273
|
+
release(Response.json({ message: "Service unavailable" }, { status: 503 }));
|
|
274
|
+
await waitFor(() => expect(screen.getByTestId("main-state").textContent).toBe("idle/stale"), {
|
|
275
|
+
timeout: 4000,
|
|
276
|
+
});
|
|
277
|
+
expect(screen.getByRole("alert").textContent).toContain("Showing previous data");
|
|
278
|
+
expect(api.resources()).toHaveLength(5);
|
|
279
|
+
view.rerender(
|
|
280
|
+
<FrappeProvider {...config}>
|
|
281
|
+
<Probe />
|
|
282
|
+
</FrappeProvider>,
|
|
283
|
+
);
|
|
284
|
+
expect(api.resources()).toHaveLength(5);
|
|
285
|
+
api.setStatus(200);
|
|
286
|
+
api.setRows([row("recovered")]);
|
|
287
|
+
const retryRelease = api.hold();
|
|
288
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry main" }));
|
|
289
|
+
await waitFor(() => expect(api.resources()).toHaveLength(6));
|
|
290
|
+
expect(screen.getByTestId("main-state").textContent).toBe("refreshing/stale");
|
|
291
|
+
retryRelease(Response.json({ data: [row("recovered")] }));
|
|
292
|
+
await expectRows("recovered:recovered");
|
|
293
|
+
await expectIdle();
|
|
294
|
+
expect(screen.queryByRole("alert")).toBeNull();
|
|
295
|
+
expect(observed.get("main")!.collection).toBe(initial);
|
|
296
|
+
expect(api.methods()).toEqual([]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it.each(["snapshot", "cdc"] as const)(
|
|
300
|
+
"routes real socket reconnect with %s strategy and preserves realtime delivery",
|
|
301
|
+
async (strategy) => {
|
|
302
|
+
const api = server();
|
|
303
|
+
render(
|
|
304
|
+
<FrappeProvider {...connection()} realtime syncOptions={{ catchUpStrategy: strategy }}>
|
|
305
|
+
<Probe />
|
|
306
|
+
</FrappeProvider>,
|
|
307
|
+
);
|
|
308
|
+
await expectRows("a:a,b:b");
|
|
309
|
+
await waitFor(() => expect(sockets).toHaveLength(1));
|
|
310
|
+
await expectIdle();
|
|
311
|
+
const initialCount = api.urls.length;
|
|
312
|
+
api.setRows([row("socket")]);
|
|
313
|
+
api.setCdc({
|
|
314
|
+
rows: [row("socket")],
|
|
315
|
+
deleted: [{ name: "a" }, { name: "b" }],
|
|
316
|
+
checkpoint: "socket-checkpoint",
|
|
317
|
+
has_more: false,
|
|
318
|
+
});
|
|
319
|
+
act(() => {
|
|
320
|
+
sockets[0].deliver("disconnect", "transport close");
|
|
321
|
+
sockets[0].deliver("connect");
|
|
322
|
+
});
|
|
323
|
+
await expectRows("socket:socket");
|
|
324
|
+
await expectIdle();
|
|
325
|
+
const catchup = api.urls.slice(initialCount);
|
|
326
|
+
expect(
|
|
327
|
+
catchup.some(
|
|
328
|
+
(url) =>
|
|
329
|
+
url.pathname ===
|
|
330
|
+
(strategy === "snapshot" ? "/api/resource/Task" : "/api/method/live.live.api.backfill"),
|
|
331
|
+
),
|
|
332
|
+
).toBe(true);
|
|
333
|
+
if (strategy === "snapshot") expect(api.methods()).toEqual([]);
|
|
334
|
+
else {
|
|
335
|
+
expect(observed.get("main")!.sync.__debug().subscriptions[0].checkpoint).toBe(
|
|
336
|
+
"socket-checkpoint",
|
|
337
|
+
);
|
|
338
|
+
fireEvent(window, new Event("online"));
|
|
339
|
+
await waitFor(() =>
|
|
340
|
+
expect(api.methods().at(-1)?.searchParams.get("modified_after")).toBe("socket-checkpoint"),
|
|
341
|
+
);
|
|
342
|
+
await expectIdle();
|
|
343
|
+
}
|
|
344
|
+
api.setRows([row("socket", "realtime edit")]);
|
|
345
|
+
act(() => {
|
|
346
|
+
sockets[0].deliver("doc_update", { doctype: "Task", name: "socket" });
|
|
347
|
+
});
|
|
348
|
+
await expectRows("socket:realtime edit");
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
it("uses CDC when omitted, observes background rejection, and keeps explicit backfill rejecting", async () => {
|
|
353
|
+
const api = server();
|
|
354
|
+
render(
|
|
355
|
+
<FrappeProvider {...connection()}>
|
|
356
|
+
<Probe />
|
|
357
|
+
</FrappeProvider>,
|
|
358
|
+
);
|
|
359
|
+
await expectRows("a:a,b:b");
|
|
360
|
+
api.setStatus(404);
|
|
361
|
+
fireEvent(window, new Event("focus"));
|
|
362
|
+
await waitFor(() => expect(screen.getByTestId("main-state").textContent).toBe("idle/stale"));
|
|
363
|
+
const sync = observed.get("main")!.sync;
|
|
364
|
+
await expect(sync.backfill({ scope: BackfillScope.All })).rejects.toMatchObject({ status: 404 });
|
|
365
|
+
expect(api.methods()[0].searchParams.get("modified_after")).toBe(row("a").modified);
|
|
366
|
+
api.setStatus(200);
|
|
367
|
+
api.setCdc({
|
|
368
|
+
rows: [row("c")],
|
|
369
|
+
deleted: [{ name: "a" }],
|
|
370
|
+
checkpoint: "recovered-checkpoint",
|
|
371
|
+
has_more: false,
|
|
372
|
+
});
|
|
373
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry main" }));
|
|
374
|
+
await expectRows("b:b,c:c");
|
|
375
|
+
await expectIdle();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("isolates same-origin strategy providers and switches collection and state together", async () => {
|
|
379
|
+
const api = server();
|
|
380
|
+
const config = connection();
|
|
381
|
+
const draw = (strategy: "snapshot" | "cdc") => (
|
|
382
|
+
<>
|
|
383
|
+
<FrappeProvider {...config} syncOptions={{ catchUpStrategy: strategy }}>
|
|
384
|
+
<Probe label="left" />
|
|
385
|
+
</FrappeProvider>
|
|
386
|
+
<FrappeProvider {...config}>
|
|
387
|
+
<Probe label="right" />
|
|
388
|
+
</FrappeProvider>
|
|
389
|
+
</>
|
|
390
|
+
);
|
|
391
|
+
const view = render(draw("snapshot"));
|
|
392
|
+
await expectRows("a:a,b:b", "left");
|
|
393
|
+
await expectRows("a:a,b:b", "right");
|
|
394
|
+
expect(observed.get("left")!.sync).not.toBe(observed.get("right")!.sync);
|
|
395
|
+
expect(observed.get("left")!.collection).not.toBe(observed.get("right")!.collection);
|
|
396
|
+
const left = observed.get("left")!.collection;
|
|
397
|
+
view.rerender(draw("snapshot"));
|
|
398
|
+
expect(observed.get("left")!.collection).toBe(left);
|
|
399
|
+
api.setRows([row("snapshot")]);
|
|
400
|
+
api.setCdc({
|
|
401
|
+
rows: [row("cdc")],
|
|
402
|
+
deleted: [{ name: "a" }, { name: "b" }],
|
|
403
|
+
checkpoint: "separate",
|
|
404
|
+
has_more: false,
|
|
405
|
+
});
|
|
406
|
+
fireEvent(window, new Event("focus"));
|
|
407
|
+
await expectRows("snapshot:snapshot", "left");
|
|
408
|
+
await expectRows("cdc:cdc", "right");
|
|
409
|
+
view.rerender(draw("cdc"));
|
|
410
|
+
await expectRows("cdc:cdc", "left");
|
|
411
|
+
expect(observed.get("left")!.collection).toBe(observed.get("right")!.collection);
|
|
412
|
+
expect(observed.get("left")!.sync).toBe(observed.get("right")!.sync);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("refuses progressive snapshot subscriptions through the public hook, with CDC control", async () => {
|
|
416
|
+
const api = server();
|
|
417
|
+
const config = connection();
|
|
418
|
+
const view = render(
|
|
419
|
+
<FrappeProvider {...config} syncOptions={{ catchUpStrategy: "snapshot" }}>
|
|
420
|
+
<InfiniteProbe />
|
|
421
|
+
</FrappeProvider>,
|
|
422
|
+
);
|
|
423
|
+
await waitFor(() =>
|
|
424
|
+
expect(screen.getByTestId("infinite").textContent).toContain("does not support progressive"),
|
|
425
|
+
);
|
|
426
|
+
expect(api.urls).toEqual([]);
|
|
427
|
+
view.rerender(
|
|
428
|
+
<FrappeProvider {...config}>
|
|
429
|
+
<InfiniteProbe />
|
|
430
|
+
</FrappeProvider>,
|
|
431
|
+
);
|
|
432
|
+
await waitFor(() => expect(screen.getByTestId("infinite").textContent).toBe("a,b"));
|
|
433
|
+
expect(api.resources()).toHaveLength(1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("keeps explicit CDC backfill rejecting even on a snapshot provider", async () => {
|
|
437
|
+
const api = server();
|
|
438
|
+
render(
|
|
439
|
+
<FrappeProvider {...connection()} syncOptions={{ catchUpStrategy: "snapshot" }}>
|
|
440
|
+
<Probe />
|
|
441
|
+
</FrappeProvider>,
|
|
442
|
+
);
|
|
443
|
+
await expectRows("a:a,b:b");
|
|
444
|
+
expect(api.methods()).toEqual([]);
|
|
445
|
+
api.setStatus(404);
|
|
446
|
+
await expect(
|
|
447
|
+
observed.get("main")!.sync.backfill({ scope: BackfillScope.All }),
|
|
448
|
+
).rejects.toMatchObject({ status: 404 });
|
|
449
|
+
expect(api.methods()).toHaveLength(1);
|
|
450
|
+
expect(api.methods()[0].pathname).toBe("/api/method/live.live.api.backfill");
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("coalesces mounted lifecycle overlap into one trailing read", async () => {
|
|
454
|
+
const api = server();
|
|
455
|
+
render(
|
|
456
|
+
<FrappeProvider {...connection()} syncOptions={{ catchUpStrategy: "snapshot" }}>
|
|
457
|
+
<Probe />
|
|
458
|
+
</FrappeProvider>,
|
|
459
|
+
);
|
|
460
|
+
await expectRows("a:a,b:b");
|
|
461
|
+
const first = api.hold();
|
|
462
|
+
fireEvent(window, new Event("focus"));
|
|
463
|
+
await waitFor(() => expect(api.resources()).toHaveLength(2));
|
|
464
|
+
const trailing = api.hold();
|
|
465
|
+
fireEvent(window, new Event("online"));
|
|
466
|
+
fireEvent.click(screen.getByRole("button", { name: "Retry main" }));
|
|
467
|
+
first(Response.json({ data: [row("earlier")] }));
|
|
468
|
+
await waitFor(() => expect(api.resources()).toHaveLength(3));
|
|
469
|
+
for (let i = 0; i < 10; i++) fireEvent(window, new Event("focus"));
|
|
470
|
+
trailing(Response.json({ data: [row("latest")] }));
|
|
471
|
+
await expectRows("latest:latest");
|
|
472
|
+
await expectIdle();
|
|
473
|
+
expect(api.resources()).toHaveLength(3);
|
|
474
|
+
expect(api.methods()).toEqual([]);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("does not publish an obsolete failure after the mounted provider is reset and unmounted", async () => {
|
|
478
|
+
const api = server();
|
|
479
|
+
const view = render(
|
|
480
|
+
<FrappeProvider {...connection()} syncOptions={{ catchUpStrategy: "snapshot" }}>
|
|
481
|
+
<Probe />
|
|
482
|
+
</FrappeProvider>,
|
|
483
|
+
);
|
|
484
|
+
await expectRows("a:a,b:b");
|
|
485
|
+
const sync = observed.get("main")!.sync;
|
|
486
|
+
const release = api.hold();
|
|
487
|
+
let pending!: ReturnType<SyncModule["catchUp"]>;
|
|
488
|
+
act(() => {
|
|
489
|
+
pending = sync.catchUp();
|
|
490
|
+
});
|
|
491
|
+
await waitFor(() => expect(api.resources()).toHaveLength(2));
|
|
492
|
+
view.unmount();
|
|
493
|
+
sync.resetCache();
|
|
494
|
+
const idle = sync.getCatchUpState();
|
|
495
|
+
const listener = vi.fn();
|
|
496
|
+
const unsubscribe = sync.subscribeStore(listener);
|
|
497
|
+
release(Response.json({ message: "obsolete" }, { status: 404 }));
|
|
498
|
+
await pending;
|
|
499
|
+
expect(sync.getCatchUpState()).toBe(idle);
|
|
500
|
+
expect(listener).not.toHaveBeenCalled();
|
|
501
|
+
unsubscribe();
|
|
502
|
+
});
|
package/src/FrappeProvider.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FixtureProvider, UseFrappeCollectionConfig } from "@multiplatform.one/frappe";
|
|
2
2
|
import React, { createContext, use, useContext } from "react";
|
|
3
|
+
import { FrappeNavigationProvider, type FrappeNavigation } from "./frappeNavigation";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Configuration for the Frappe connection.
|
|
@@ -49,7 +50,7 @@ export interface FrappeConfig {
|
|
|
49
50
|
type?: string;
|
|
50
51
|
};
|
|
51
52
|
/** Sync options for the live data engine */
|
|
52
|
-
syncOptions?:
|
|
53
|
+
syncOptions?: UseFrappeCollectionConfig["syncOptions"];
|
|
53
54
|
/**
|
|
54
55
|
* Extra Socket.IO client options for the realtime connection — e.g.
|
|
55
56
|
* `{ transports: ["polling", "websocket"] }` behind dev proxies that
|
|
@@ -93,7 +94,7 @@ export interface FrappeProviderProps {
|
|
|
93
94
|
type?: string;
|
|
94
95
|
};
|
|
95
96
|
/** Sync options for the live data engine */
|
|
96
|
-
syncOptions?:
|
|
97
|
+
syncOptions?: UseFrappeCollectionConfig["syncOptions"];
|
|
97
98
|
/** Extra Socket.IO client options for the realtime connection. */
|
|
98
99
|
socketOptions?: UseFrappeCollectionConfig["socketOptions"];
|
|
99
100
|
/**
|
|
@@ -101,6 +102,12 @@ export interface FrappeProviderProps {
|
|
|
101
102
|
* `baseURL` to run the whole app off fixtures with no server.
|
|
102
103
|
*/
|
|
103
104
|
fixtures?: FixtureProvider;
|
|
105
|
+
/**
|
|
106
|
+
* The host router. Given, a `FrappeListView` or `FrappeForm` with a
|
|
107
|
+
* `route` reads its record from the path and navigates between the
|
|
108
|
+
* doctype's list, record, edit and new pages on its own.
|
|
109
|
+
*/
|
|
110
|
+
navigation?: FrappeNavigation;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
/**
|
|
@@ -137,6 +144,7 @@ export function FrappeProvider({
|
|
|
137
144
|
syncOptions,
|
|
138
145
|
socketOptions,
|
|
139
146
|
fixtures,
|
|
147
|
+
navigation,
|
|
140
148
|
}: FrappeProviderProps) {
|
|
141
149
|
const value = React.useMemo(
|
|
142
150
|
() => ({
|
|
@@ -163,7 +171,11 @@ export function FrappeProvider({
|
|
|
163
171
|
],
|
|
164
172
|
);
|
|
165
173
|
|
|
166
|
-
return
|
|
174
|
+
return (
|
|
175
|
+
<FrappeContext.Provider value={value}>
|
|
176
|
+
<FrappeNavigationProvider navigation={navigation}>{children}</FrappeNavigationProvider>
|
|
177
|
+
</FrappeContext.Provider>
|
|
178
|
+
);
|
|
167
179
|
}
|
|
168
180
|
|
|
169
181
|
/**
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useDocTypeMeta,
|
|
3
|
-
useFrappeCollection,
|
|
4
|
-
useFrappeDoctypeEvents,
|
|
5
|
-
useLiveQuery,
|
|
6
|
-
} from "@multiplatform.one/frappe";
|
|
1
|
+
import { useDocTypeMeta, useFrappeDoctypeEvents } from "@multiplatform.one/frappe";
|
|
7
2
|
import { downloadBlob } from "@multiplatform.one/platform";
|
|
8
3
|
import {
|
|
9
4
|
ReportBuilder,
|
|
@@ -20,6 +15,7 @@ import { Popover, Text, XStack, YStack, type YStackProps } from "tamagui";
|
|
|
20
15
|
import { FloppyDiskIcon, FolderOpenIcon, TrashIcon } from "@phosphor-icons/react";
|
|
21
16
|
import { useResolvedKnobs } from "@multiplatform.one/theme";
|
|
22
17
|
import { useFrappeConfig } from "./FrappeProvider";
|
|
18
|
+
import { useFrappeRows } from "./useFrappeRows";
|
|
23
19
|
|
|
24
20
|
// ============================================================================
|
|
25
21
|
// Saved Report Types (Frappe-specific persisted storage)
|
|
@@ -164,7 +160,7 @@ export function FrappeReportBuilder({
|
|
|
164
160
|
fieldname: f.fieldname,
|
|
165
161
|
label: f.label || f.fieldname,
|
|
166
162
|
fieldtype: f.fieldtype,
|
|
167
|
-
enabled: f.in_list_view
|
|
163
|
+
enabled: !!f.in_list_view,
|
|
168
164
|
}));
|
|
169
165
|
}, [initialColumns, meta]);
|
|
170
166
|
|
|
@@ -177,20 +173,16 @@ export function FrappeReportBuilder({
|
|
|
177
173
|
return fieldNames as readonly string[];
|
|
178
174
|
}, [allColumns]);
|
|
179
175
|
|
|
180
|
-
const collection = useFrappeCollection<{ name: string; doctype: string }>(
|
|
181
|
-
connectionConfig,
|
|
182
|
-
doctype,
|
|
183
|
-
{
|
|
184
|
-
fields: fetchFields as readonly ("name" | "doctype")[],
|
|
185
|
-
filters: filters as Partial<{ name: string; doctype: string }>,
|
|
186
|
-
},
|
|
187
|
-
);
|
|
188
176
|
const {
|
|
189
|
-
|
|
190
|
-
isLoading,
|
|
177
|
+
rows,
|
|
178
|
+
isQueryLoading: isLoading,
|
|
191
179
|
isError,
|
|
192
|
-
} =
|
|
193
|
-
|
|
180
|
+
} = useFrappeRows<{ name: string; doctype: string }>(connectionConfig, doctype, {
|
|
181
|
+
fields: fetchFields as readonly ("name" | "doctype")[],
|
|
182
|
+
filters: filters as Partial<{ name: string; doctype: string }>,
|
|
183
|
+
limit: null,
|
|
184
|
+
});
|
|
185
|
+
const data = rows as unknown as Record<string, unknown>[];
|
|
194
186
|
|
|
195
187
|
// Live staleness: a realtime change on the doctype marks the current
|
|
196
188
|
// report result stale until the collection reconciles (data identity
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useFrappeRows,
|
|
4
|
+
type FrappeRowsConnection,
|
|
5
|
+
type UseFrappeRowsOptions,
|
|
6
|
+
} from "./useFrappeRows";
|
|
7
|
+
|
|
8
|
+
export interface FrappeRowsSnapshot<TDoc> {
|
|
9
|
+
rows: TDoc[];
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FrappeRowsReaderProps<TDoc extends { name: string; doctype: string }> {
|
|
15
|
+
connection: FrappeRowsConnection | undefined;
|
|
16
|
+
doctype: string;
|
|
17
|
+
/** Memoize it: a new identity every render re-reports the same rows. */
|
|
18
|
+
options: UseFrappeRowsOptions<TDoc>;
|
|
19
|
+
onChange: (snapshot: FrappeRowsSnapshot<TDoc>) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One doctype read as a component. A parent that needs a varying number of
|
|
24
|
+
* reads (one per connection, one per junction filter) mounts one reader per
|
|
25
|
+
* read instead of calling hooks in a loop. Renders nothing.
|
|
26
|
+
*/
|
|
27
|
+
export function FrappeRowsReader<TDoc extends { name: string; doctype: string }>({
|
|
28
|
+
connection,
|
|
29
|
+
doctype,
|
|
30
|
+
options,
|
|
31
|
+
onChange,
|
|
32
|
+
}: FrappeRowsReaderProps<TDoc>) {
|
|
33
|
+
const { rows, isLoading, error } = useFrappeRows<TDoc>(connection, doctype, options);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
onChange({ rows, isLoading, error });
|
|
36
|
+
}, [rows, isLoading, error, onChange]);
|
|
37
|
+
return null;
|
|
38
|
+
}
|