@multiplatform.one/frappe-ui 7.28.1 → 7.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/FrappeCalendar.cjs +3 -2
- package/dist/cjs/FrappeCalendar.native.js +3 -2
- package/dist/cjs/FrappeCalendar.native.js.map +1 -1
- package/dist/cjs/FrappeForm.cjs +148 -464
- package/dist/cjs/FrappeForm.native.js +152 -633
- package/dist/cjs/FrappeForm.native.js.map +1 -1
- package/dist/cjs/FrappeListView.cjs +5 -27
- package/dist/cjs/FrappeListView.native.js +11 -32
- package/dist/cjs/FrappeListView.native.js.map +1 -1
- package/dist/cjs/FrappeReportBuilder.cjs +523 -309
- package/dist/cjs/FrappeReportBuilder.native.js +658 -353
- package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
- package/dist/cjs/FrappeReportView.cjs +35 -234
- package/dist/cjs/FrappeReportView.native.js +39 -283
- package/dist/cjs/FrappeReportView.native.js.map +1 -1
- package/dist/cjs/dependsOn.cjs +2 -2
- package/dist/cjs/dependsOn.native.js +1 -2
- package/dist/cjs/dependsOn.native.js.map +1 -1
- package/dist/cjs/fields/shared/EditorPreviewPanes.cjs +2 -2
- package/dist/cjs/fields/shared/EditorPreviewPanes.native.js +2 -2
- package/dist/cjs/fields/shared/EditorPreviewPanes.native.js.map +1 -1
- package/dist/cjs/formEngine/docPermissions.cjs +20 -0
- package/dist/cjs/formEngine/docPermissions.native.js +69 -0
- package/dist/cjs/formEngine/docPermissions.native.js.map +1 -1
- package/dist/cjs/formEngine/index.cjs +2 -0
- package/dist/cjs/formEngine/index.native.js +2 -0
- package/dist/cjs/formEngine/index.native.js.map +1 -1
- package/dist/cjs/index.cjs +67 -7
- package/dist/cjs/index.native.js +67 -7
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/liveErrors.cjs +8 -2
- package/dist/cjs/liveErrors.native.js +8 -2
- package/dist/cjs/liveErrors.native.js.map +1 -1
- package/dist/cjs/measures.cjs +31 -0
- package/dist/cjs/measures.native.js +34 -0
- package/dist/cjs/measures.native.js.map +1 -0
- package/dist/cjs/recordShape.cjs +103 -0
- package/dist/cjs/recordShape.native.js +115 -0
- package/dist/cjs/recordShape.native.js.map +1 -0
- package/dist/cjs/report/ReportGrid.cjs +453 -0
- package/dist/cjs/report/ReportGrid.native.js +510 -0
- package/dist/cjs/report/ReportGrid.native.js.map +1 -0
- package/dist/cjs/report/ReportViewToolbar.cjs +253 -0
- package/dist/cjs/report/ReportViewToolbar.native.js +303 -0
- package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -0
- package/dist/cjs/report/queryReport.cjs +444 -0
- package/dist/cjs/report/queryReport.native.js +588 -0
- package/dist/cjs/report/queryReport.native.js.map +1 -0
- package/dist/cjs/report/queryReportColumns.cjs +131 -0
- package/dist/cjs/report/queryReportColumns.native.js +159 -0
- package/dist/cjs/report/queryReportColumns.native.js.map +1 -0
- package/dist/cjs/report/reportViewModel.cjs +386 -0
- package/dist/cjs/report/reportViewModel.native.js +657 -0
- package/dist/cjs/report/reportViewModel.native.js.map +1 -0
- package/dist/cjs/report/savedReports.cjs +137 -0
- package/dist/cjs/report/savedReports.native.js +169 -0
- package/dist/cjs/report/savedReports.native.js.map +1 -0
- package/dist/cjs/report/useFrappeReport.cjs +184 -0
- package/dist/cjs/report/useFrappeReport.native.js +214 -0
- package/dist/cjs/report/useFrappeReport.native.js.map +1 -0
- package/dist/cjs/report/useQueryReport.cjs +201 -0
- package/dist/cjs/report/useQueryReport.native.js +233 -0
- package/dist/cjs/report/useQueryReport.native.js.map +1 -0
- package/dist/cjs/useFrappeForm.cjs +558 -0
- package/dist/cjs/useFrappeForm.native.js +714 -0
- package/dist/cjs/useFrappeForm.native.js.map +1 -0
- package/dist/esm/FrappeCalendar.mjs +4 -3
- package/dist/esm/FrappeCalendar.mjs.map +1 -1
- package/dist/esm/FrappeCalendar.native.js +4 -3
- package/dist/esm/FrappeCalendar.native.js.map +1 -1
- package/dist/esm/FrappeForm.mjs +151 -467
- package/dist/esm/FrappeForm.mjs.map +1 -1
- package/dist/esm/FrappeForm.native.js +155 -636
- package/dist/esm/FrappeForm.native.js.map +1 -1
- package/dist/esm/FrappeListView.mjs +6 -28
- package/dist/esm/FrappeListView.mjs.map +1 -1
- package/dist/esm/FrappeListView.native.js +12 -33
- package/dist/esm/FrappeListView.native.js.map +1 -1
- package/dist/esm/FrappeReportBuilder.mjs +526 -312
- package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
- package/dist/esm/FrappeReportBuilder.native.js +661 -356
- package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
- package/dist/esm/FrappeReportView.mjs +30 -229
- package/dist/esm/FrappeReportView.mjs.map +1 -1
- package/dist/esm/FrappeReportView.native.js +34 -278
- package/dist/esm/FrappeReportView.native.js.map +1 -1
- package/dist/esm/dependsOn.mjs +2 -2
- package/dist/esm/dependsOn.mjs.map +1 -1
- package/dist/esm/dependsOn.native.js +1 -2
- package/dist/esm/dependsOn.native.js.map +1 -1
- package/dist/esm/fields/shared/EditorPreviewPanes.mjs +2 -2
- package/dist/esm/fields/shared/EditorPreviewPanes.mjs.map +1 -1
- package/dist/esm/fields/shared/EditorPreviewPanes.native.js +2 -2
- package/dist/esm/fields/shared/EditorPreviewPanes.native.js.map +1 -1
- package/dist/esm/formEngine/docPermissions.mjs +19 -1
- package/dist/esm/formEngine/docPermissions.mjs.map +1 -1
- package/dist/esm/formEngine/docPermissions.native.js +68 -1
- package/dist/esm/formEngine/docPermissions.native.js.map +1 -1
- package/dist/esm/formEngine/index.mjs +2 -2
- package/dist/esm/formEngine/index.mjs.map +1 -1
- package/dist/esm/formEngine/index.native.js +2 -2
- package/dist/esm/formEngine/index.native.js.map +1 -1
- package/dist/esm/index.mjs +16 -4
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +16 -4
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/liveErrors.mjs +8 -3
- package/dist/esm/liveErrors.mjs.map +1 -1
- package/dist/esm/liveErrors.native.js +8 -3
- package/dist/esm/liveErrors.native.js.map +1 -1
- package/dist/esm/measures.mjs +5 -0
- package/dist/esm/measures.mjs.map +1 -0
- package/dist/esm/measures.native.js +5 -0
- package/dist/esm/measures.native.js.map +1 -0
- package/dist/esm/recordShape.mjs +74 -0
- package/dist/esm/recordShape.mjs.map +1 -0
- package/dist/esm/recordShape.native.js +83 -0
- package/dist/esm/recordShape.native.js.map +1 -0
- package/dist/esm/report/ReportGrid.mjs +428 -0
- package/dist/esm/report/ReportGrid.mjs.map +1 -0
- package/dist/esm/report/ReportGrid.native.js +471 -0
- package/dist/esm/report/ReportGrid.native.js.map +1 -0
- package/dist/esm/report/ReportViewToolbar.mjs +228 -0
- package/dist/esm/report/ReportViewToolbar.mjs.map +1 -0
- package/dist/esm/report/ReportViewToolbar.native.js +264 -0
- package/dist/esm/report/ReportViewToolbar.native.js.map +1 -0
- package/dist/esm/report/queryReport.mjs +404 -0
- package/dist/esm/report/queryReport.mjs.map +1 -0
- package/dist/esm/report/queryReport.native.js +545 -0
- package/dist/esm/report/queryReport.native.js.map +1 -0
- package/dist/esm/report/queryReportColumns.mjs +105 -0
- package/dist/esm/report/queryReportColumns.mjs.map +1 -0
- package/dist/esm/report/queryReportColumns.native.js +119 -0
- package/dist/esm/report/queryReportColumns.native.js.map +1 -0
- package/dist/esm/report/reportViewModel.mjs +343 -0
- package/dist/esm/report/reportViewModel.mjs.map +1 -0
- package/dist/esm/report/reportViewModel.native.js +611 -0
- package/dist/esm/report/reportViewModel.native.js.map +1 -0
- package/dist/esm/report/savedReports.mjs +112 -0
- package/dist/esm/report/savedReports.mjs.map +1 -0
- package/dist/esm/report/savedReports.native.js +141 -0
- package/dist/esm/report/savedReports.native.js.map +1 -0
- package/dist/esm/report/useFrappeReport.mjs +158 -0
- package/dist/esm/report/useFrappeReport.mjs.map +1 -0
- package/dist/esm/report/useFrappeReport.native.js +185 -0
- package/dist/esm/report/useFrappeReport.native.js.map +1 -0
- package/dist/esm/report/useQueryReport.mjs +176 -0
- package/dist/esm/report/useQueryReport.mjs.map +1 -0
- package/dist/esm/report/useQueryReport.native.js +205 -0
- package/dist/esm/report/useQueryReport.native.js.map +1 -0
- package/dist/esm/useFrappeForm.mjs +533 -0
- package/dist/esm/useFrappeForm.mjs.map +1 -0
- package/dist/esm/useFrappeForm.native.js +686 -0
- package/dist/esm/useFrappeForm.native.js.map +1 -0
- package/dist/jsx/FrappeCalendar.mjs +4 -3
- package/dist/jsx/FrappeCalendar.mjs.map +1 -1
- package/dist/jsx/FrappeCalendar.native.js +3 -2
- package/dist/jsx/FrappeCalendar.native.js.map +1 -1
- package/dist/jsx/FrappeForm.mjs +151 -467
- package/dist/jsx/FrappeForm.mjs.map +1 -1
- package/dist/jsx/FrappeForm.native.js +152 -633
- package/dist/jsx/FrappeForm.native.js.map +1 -1
- package/dist/jsx/FrappeListView.mjs +6 -28
- package/dist/jsx/FrappeListView.mjs.map +1 -1
- package/dist/jsx/FrappeListView.native.js +11 -32
- package/dist/jsx/FrappeListView.native.js.map +1 -1
- package/dist/jsx/FrappeReportBuilder.mjs +526 -312
- package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
- package/dist/jsx/FrappeReportBuilder.native.js +658 -353
- package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
- package/dist/jsx/FrappeReportView.mjs +30 -229
- package/dist/jsx/FrappeReportView.mjs.map +1 -1
- package/dist/jsx/FrappeReportView.native.js +39 -283
- package/dist/jsx/FrappeReportView.native.js.map +1 -1
- package/dist/jsx/dependsOn.mjs +2 -2
- package/dist/jsx/dependsOn.mjs.map +1 -1
- package/dist/jsx/dependsOn.native.js +1 -2
- package/dist/jsx/dependsOn.native.js.map +1 -1
- package/dist/jsx/fields/shared/EditorPreviewPanes.mjs +2 -2
- package/dist/jsx/fields/shared/EditorPreviewPanes.mjs.map +1 -1
- package/dist/jsx/fields/shared/EditorPreviewPanes.native.js +2 -2
- package/dist/jsx/fields/shared/EditorPreviewPanes.native.js.map +1 -1
- package/dist/jsx/formEngine/docPermissions.mjs +19 -1
- package/dist/jsx/formEngine/docPermissions.mjs.map +1 -1
- package/dist/jsx/formEngine/docPermissions.native.js +69 -0
- package/dist/jsx/formEngine/docPermissions.native.js.map +1 -1
- package/dist/jsx/formEngine/index.mjs +2 -2
- package/dist/jsx/formEngine/index.mjs.map +1 -1
- package/dist/jsx/formEngine/index.native.js +2 -0
- package/dist/jsx/formEngine/index.native.js.map +1 -1
- package/dist/jsx/index.js +16 -4
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +16 -4
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +67 -7
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/liveErrors.mjs +8 -3
- package/dist/jsx/liveErrors.mjs.map +1 -1
- package/dist/jsx/liveErrors.native.js +8 -2
- package/dist/jsx/liveErrors.native.js.map +1 -1
- package/dist/jsx/measures.mjs +5 -0
- package/dist/jsx/measures.mjs.map +1 -0
- package/dist/jsx/measures.native.js +34 -0
- package/dist/jsx/measures.native.js.map +1 -0
- package/dist/jsx/recordShape.mjs +74 -0
- package/dist/jsx/recordShape.mjs.map +1 -0
- package/dist/jsx/recordShape.native.js +115 -0
- package/dist/jsx/recordShape.native.js.map +1 -0
- package/dist/jsx/report/ReportGrid.mjs +428 -0
- package/dist/jsx/report/ReportGrid.mjs.map +1 -0
- package/dist/jsx/report/ReportGrid.native.js +510 -0
- package/dist/jsx/report/ReportGrid.native.js.map +1 -0
- package/dist/jsx/report/ReportViewToolbar.mjs +228 -0
- package/dist/jsx/report/ReportViewToolbar.mjs.map +1 -0
- package/dist/jsx/report/ReportViewToolbar.native.js +303 -0
- package/dist/jsx/report/ReportViewToolbar.native.js.map +1 -0
- package/dist/jsx/report/queryReport.mjs +404 -0
- package/dist/jsx/report/queryReport.mjs.map +1 -0
- package/dist/jsx/report/queryReport.native.js +588 -0
- package/dist/jsx/report/queryReport.native.js.map +1 -0
- package/dist/jsx/report/queryReportColumns.mjs +105 -0
- package/dist/jsx/report/queryReportColumns.mjs.map +1 -0
- package/dist/jsx/report/queryReportColumns.native.js +159 -0
- package/dist/jsx/report/queryReportColumns.native.js.map +1 -0
- package/dist/jsx/report/reportViewModel.mjs +343 -0
- package/dist/jsx/report/reportViewModel.mjs.map +1 -0
- package/dist/jsx/report/reportViewModel.native.js +657 -0
- package/dist/jsx/report/reportViewModel.native.js.map +1 -0
- package/dist/jsx/report/savedReports.mjs +112 -0
- package/dist/jsx/report/savedReports.mjs.map +1 -0
- package/dist/jsx/report/savedReports.native.js +169 -0
- package/dist/jsx/report/savedReports.native.js.map +1 -0
- package/dist/jsx/report/useFrappeReport.mjs +158 -0
- package/dist/jsx/report/useFrappeReport.mjs.map +1 -0
- package/dist/jsx/report/useFrappeReport.native.js +214 -0
- package/dist/jsx/report/useFrappeReport.native.js.map +1 -0
- package/dist/jsx/report/useQueryReport.mjs +176 -0
- package/dist/jsx/report/useQueryReport.mjs.map +1 -0
- package/dist/jsx/report/useQueryReport.native.js +233 -0
- package/dist/jsx/report/useQueryReport.native.js.map +1 -0
- package/dist/jsx/useFrappeForm.mjs +533 -0
- package/dist/jsx/useFrappeForm.mjs.map +1 -0
- package/dist/jsx/useFrappeForm.native.js +714 -0
- package/dist/jsx/useFrappeForm.native.js.map +1 -0
- package/package.json +16 -16
- package/src/FrappeCalendar.tsx +8 -2
- package/src/FrappeForm.stories.tsx +52 -0
- package/src/FrappeForm.tsx +176 -565
- package/src/FrappeListView.cards.stories.tsx +3 -6
- package/src/FrappeListView.tsx +15 -55
- package/src/FrappeReportBuilder.stories.tsx +34 -7
- package/src/FrappeReportBuilder.tsx +790 -380
- package/src/FrappeReportView.spec.tsx +10 -6
- package/src/FrappeReportView.stories.tsx +20 -1
- package/src/FrappeReportView.tsx +89 -415
- package/src/dependsOn.ts +4 -1
- package/src/fields/shared/EditorPreviewPanes.tsx +2 -2
- package/src/formEngine/docPermissions.spec.ts +36 -1
- package/src/formEngine/docPermissions.ts +34 -0
- package/src/formEngine/index.ts +8 -1
- package/src/frappeRemainder.designLaw.spec.ts +9 -1
- package/src/frappeRoutes.spec.tsx +9 -0
- package/src/frappeViews.designLaw.spec.ts +15 -2
- package/src/guestFallback.spec.tsx +236 -0
- package/src/index.ts +117 -16
- package/src/liveErrors.spec.ts +10 -0
- package/src/liveErrors.ts +16 -3
- package/src/measures.ts +11 -0
- package/src/recordShape.spec.ts +100 -0
- package/src/recordShape.ts +133 -0
- package/src/report/ReportGrid.spec.tsx +175 -0
- package/src/report/ReportGrid.tsx +530 -0
- package/src/report/ReportViewToolbar.spec.tsx +90 -0
- package/src/report/ReportViewToolbar.tsx +314 -0
- package/src/report/queryReport.spec.ts +483 -0
- package/src/report/queryReport.ts +722 -0
- package/src/report/queryReportColumns.tsx +123 -0
- package/src/report/reportViewModel.spec.ts +393 -0
- package/src/report/reportViewModel.ts +617 -0
- package/src/report/savedReports.ts +172 -0
- package/src/report/seam.spec.ts +216 -0
- package/src/report/useFrappeReport.ts +241 -0
- package/src/report/useQueryReport.spec.tsx +131 -0
- package/src/report/useQueryReport.ts +309 -0
- package/src/useFrappeForm.spec.tsx +209 -0
- package/src/useFrappeForm.ts +778 -0
- package/types/FrappeCalendar.d.ts.map +1 -1
- package/types/FrappeForm.d.ts +21 -1
- package/types/FrappeForm.d.ts.map +1 -1
- package/types/FrappeListView.d.ts.map +1 -1
- package/types/FrappeReportBuilder.d.ts +66 -16
- package/types/FrappeReportBuilder.d.ts.map +1 -1
- package/types/FrappeReportView.d.ts +9 -127
- package/types/FrappeReportView.d.ts.map +1 -1
- package/types/dependsOn.d.ts.map +1 -1
- package/types/formEngine/docPermissions.d.ts +14 -0
- package/types/formEngine/docPermissions.d.ts.map +1 -1
- package/types/formEngine/index.d.ts +1 -1
- package/types/formEngine/index.d.ts.map +1 -1
- package/types/index.d.ts +15 -3
- package/types/index.d.ts.map +1 -1
- package/types/liveErrors.d.ts +9 -1
- package/types/liveErrors.d.ts.map +1 -1
- package/types/measures.d.ts +3 -0
- package/types/measures.d.ts.map +1 -0
- package/types/recordShape.d.ts +45 -0
- package/types/recordShape.d.ts.map +1 -0
- package/types/report/ReportGrid.d.ts +20 -0
- package/types/report/ReportGrid.d.ts.map +1 -0
- package/types/report/ReportViewToolbar.d.ts +31 -0
- package/types/report/ReportViewToolbar.d.ts.map +1 -0
- package/types/report/queryReport.d.ts +181 -0
- package/types/report/queryReport.d.ts.map +1 -0
- package/types/report/queryReportColumns.d.ts +12 -0
- package/types/report/queryReportColumns.d.ts.map +1 -0
- package/types/report/reportViewModel.d.ts +146 -0
- package/types/report/reportViewModel.d.ts.map +1 -0
- package/types/report/savedReports.d.ts +27 -0
- package/types/report/savedReports.d.ts.map +1 -0
- package/types/report/useFrappeReport.d.ts +60 -0
- package/types/report/useFrappeReport.d.ts.map +1 -0
- package/types/report/useQueryReport.d.ts +37 -0
- package/types/report/useQueryReport.d.ts.map +1 -0
- package/types/useFrappeForm.d.ts +142 -0
- package/types/useFrappeForm.d.ts.map +1 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Saved reports — desk parity via the `Report` doctype (report_type
|
|
2
|
+
// "Report Builder"): the same records desk's "Select Report" dropdown
|
|
3
|
+
// lists. Save/Save As ride the whitelisted frappe.desk.reportview
|
|
4
|
+
// save_report / delete_report methods (identical payload shape, so desk
|
|
5
|
+
// opens reports saved here and vice versa). Fixture mode (stories / no
|
|
6
|
+
// bench) keeps an in-memory list so the control stays demonstrable.
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { useFrappeConfig } from "../FrappeProvider";
|
|
9
|
+
import {
|
|
10
|
+
toDeskReportSettings,
|
|
11
|
+
fromDeskReportSettings,
|
|
12
|
+
type ReportViewConfig,
|
|
13
|
+
} from "./reportViewModel";
|
|
14
|
+
import { reportCall, type FrappeReportConnection } from "./useFrappeReport";
|
|
15
|
+
|
|
16
|
+
export interface SavedReportSummary {
|
|
17
|
+
/** Report document name (usually equals the report title). */
|
|
18
|
+
name: string;
|
|
19
|
+
reportName: string;
|
|
20
|
+
json?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseSavedReportsResult {
|
|
24
|
+
reports: SavedReportSummary[];
|
|
25
|
+
loading: boolean;
|
|
26
|
+
/** Load a saved report's view config (fields/filters/sort/…). */
|
|
27
|
+
load: (name: string) => Promise<ReportViewConfig | undefined>;
|
|
28
|
+
/** Create or update a Report doc; returns the server-side name. */
|
|
29
|
+
save: (name: string, config: ReportViewConfig) => Promise<string>;
|
|
30
|
+
remove: (name: string) => Promise<void>;
|
|
31
|
+
refresh: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ReportListRow = { name: string; report_name?: string; json?: string };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Where saved reports are read and written. Omitted, the ambient
|
|
38
|
+
* FrappeProvider's connection; `fixtures` keeps them in memory.
|
|
39
|
+
*/
|
|
40
|
+
export interface SavedReportsConnection extends FrappeReportConnection {
|
|
41
|
+
fixtures?: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useSavedReports(
|
|
45
|
+
doctype: string,
|
|
46
|
+
connection?: SavedReportsConnection,
|
|
47
|
+
): UseSavedReportsResult {
|
|
48
|
+
const config = useFrappeConfig();
|
|
49
|
+
const baseURL = connection ? connection.baseURL : config?.baseURL;
|
|
50
|
+
const auth = connection ? connection.auth : config?.auth;
|
|
51
|
+
const fixtureMode = Boolean(connection ? connection.fixtures : config?.fixtures);
|
|
52
|
+
const call = useMemo(
|
|
53
|
+
() => (baseURL ? reportCall({ baseURL, auth }) : undefined),
|
|
54
|
+
[baseURL, auth],
|
|
55
|
+
);
|
|
56
|
+
const [reports, setReports] = useState<SavedReportSummary[]>([]);
|
|
57
|
+
const [loading, setLoading] = useState(false);
|
|
58
|
+
const [generation, setGeneration] = useState(0);
|
|
59
|
+
// Fixture-mode saves live here (stories/tests) — never reaches a bench.
|
|
60
|
+
const memoryConfigs = useRef(new Map<string, ReportViewConfig>());
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
memoryConfigs.current = new Map();
|
|
64
|
+
setReports([]);
|
|
65
|
+
}, [doctype]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!call || fixtureMode) return;
|
|
69
|
+
let cancelled = false;
|
|
70
|
+
setLoading(true);
|
|
71
|
+
call
|
|
72
|
+
.get<{ message?: ReportListRow[] }>("frappe.client.get_list", {
|
|
73
|
+
doctype: "Report",
|
|
74
|
+
filters: JSON.stringify([
|
|
75
|
+
["ref_doctype", "=", doctype],
|
|
76
|
+
["report_type", "=", "Report Builder"],
|
|
77
|
+
["disabled", "=", 0],
|
|
78
|
+
]),
|
|
79
|
+
fields: JSON.stringify(["name", "report_name", "json"]),
|
|
80
|
+
limit_page_length: 0,
|
|
81
|
+
order_by: "report_name asc",
|
|
82
|
+
})
|
|
83
|
+
.then((res) => {
|
|
84
|
+
if (cancelled) return;
|
|
85
|
+
const rows = res.payload?.message ?? [];
|
|
86
|
+
setReports(
|
|
87
|
+
rows.map((row) => ({
|
|
88
|
+
name: row.name,
|
|
89
|
+
reportName: row.report_name || row.name,
|
|
90
|
+
json: row.json,
|
|
91
|
+
})),
|
|
92
|
+
);
|
|
93
|
+
setLoading(false);
|
|
94
|
+
})
|
|
95
|
+
.catch(() => {
|
|
96
|
+
// Report doctype may be unreadable for restricted roles — the
|
|
97
|
+
// selector then simply offers no saved reports.
|
|
98
|
+
if (cancelled) return;
|
|
99
|
+
setReports([]);
|
|
100
|
+
setLoading(false);
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
cancelled = true;
|
|
104
|
+
};
|
|
105
|
+
}, [call, fixtureMode, doctype, generation]);
|
|
106
|
+
|
|
107
|
+
const load = useCallback(
|
|
108
|
+
async (name: string): Promise<ReportViewConfig | undefined> => {
|
|
109
|
+
if (fixtureMode) return memoryConfigs.current.get(name);
|
|
110
|
+
const summary = reports.find((report) => report.name === name);
|
|
111
|
+
let json = summary?.json;
|
|
112
|
+
if (json === undefined && call) {
|
|
113
|
+
const res = await call.get<{ message?: { json?: string } }>("frappe.client.get_value", {
|
|
114
|
+
doctype: "Report",
|
|
115
|
+
filters: JSON.stringify({ name }),
|
|
116
|
+
fieldname: JSON.stringify(["json"]),
|
|
117
|
+
});
|
|
118
|
+
json = res.payload?.message?.json;
|
|
119
|
+
}
|
|
120
|
+
if (!json) return undefined;
|
|
121
|
+
try {
|
|
122
|
+
return fromDeskReportSettings(doctype, JSON.parse(json));
|
|
123
|
+
} catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[call, fixtureMode, reports, doctype],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const save = useCallback(
|
|
131
|
+
async (name: string, viewConfig: ReportViewConfig): Promise<string> => {
|
|
132
|
+
const trimmed = name.trim();
|
|
133
|
+
if (!trimmed) throw new Error("Report name is required");
|
|
134
|
+
if (fixtureMode || !call) {
|
|
135
|
+
memoryConfigs.current.set(trimmed, viewConfig);
|
|
136
|
+
setReports((previous) => [
|
|
137
|
+
...previous.filter((report) => report.name !== trimmed),
|
|
138
|
+
{ name: trimmed, reportName: trimmed },
|
|
139
|
+
]);
|
|
140
|
+
return trimmed;
|
|
141
|
+
}
|
|
142
|
+
const res = await call.post<{ message?: string }>("frappe.desk.reportview.save_report", {
|
|
143
|
+
name: trimmed,
|
|
144
|
+
doctype,
|
|
145
|
+
report_settings: JSON.stringify(toDeskReportSettings(doctype, viewConfig)),
|
|
146
|
+
});
|
|
147
|
+
setGeneration((value) => value + 1);
|
|
148
|
+
return res.payload?.message ?? trimmed;
|
|
149
|
+
},
|
|
150
|
+
[call, fixtureMode, doctype],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const remove = useCallback(
|
|
154
|
+
async (name: string): Promise<void> => {
|
|
155
|
+
if (fixtureMode || !call) {
|
|
156
|
+
memoryConfigs.current.delete(name);
|
|
157
|
+
setReports((previous) => previous.filter((report) => report.name !== name));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await call.post("frappe.desk.reportview.delete_report", { name });
|
|
161
|
+
setGeneration((value) => value + 1);
|
|
162
|
+
},
|
|
163
|
+
[call, fixtureMode],
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const refresh = useCallback(() => setGeneration((value) => value + 1), []);
|
|
167
|
+
|
|
168
|
+
return useMemo(
|
|
169
|
+
() => ({ reports, loading, load, save, remove, refresh }),
|
|
170
|
+
[reports, loading, load, save, remove, refresh],
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPO-15 (GL-28) — the report seam, from this side.
|
|
3
|
+
*
|
|
4
|
+
* Two report concepts, a named query/script report and a doctype's report
|
|
5
|
+
* view, used to carry four models: FrappeReportView and FrappeReportBuilder
|
|
6
|
+
* here, reportShapes and reportViewModel plus DoctypeReportView one tier up
|
|
7
|
+
* in `@multiplatform.one/backoffice`. One model per concept lives here now,
|
|
8
|
+
* and four things have to stay true for that to mean anything:
|
|
9
|
+
*
|
|
10
|
+
* 1. The package barrel hands every name out.
|
|
11
|
+
* 2. The report modules reach nothing above them: no backoffice import and
|
|
12
|
+
* no bounce back through this package's own barrel.
|
|
13
|
+
* 3. Each model is declared once across both packages, and scrub is the
|
|
14
|
+
* server's `frappe.scrub`.
|
|
15
|
+
* 4. The exported components render through the models: FrappeReportView
|
|
16
|
+
* through the query-report model, FrappeReportBuilder through the
|
|
17
|
+
* report-view model.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
|
|
23
|
+
const reportDir = path.resolve(__dirname);
|
|
24
|
+
const srcDir = path.resolve(__dirname, "..");
|
|
25
|
+
const publicDir = path.resolve(srcDir, "../..");
|
|
26
|
+
|
|
27
|
+
function sourceFiles(dir: string): string[] {
|
|
28
|
+
const out: string[] = [];
|
|
29
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
const full = path.join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
out.push(...sourceFiles(full));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!/\.(ts|tsx)$/.test(entry.name)) continue;
|
|
36
|
+
if (/\.(spec|stories)\./.test(entry.name)) continue;
|
|
37
|
+
out.push(full);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function read(rel: string): string {
|
|
43
|
+
return fs.readFileSync(path.resolve(srcDir, rel), "utf-8");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function declaredIn(pattern: RegExp): string[] {
|
|
47
|
+
return [...sourceFiles(srcDir), ...sourceFiles(path.resolve(publicDir, "backoffice/src"))]
|
|
48
|
+
.filter((file) => pattern.test(fs.readFileSync(file, "utf-8")))
|
|
49
|
+
.map((file) => path.relative(publicDir, file))
|
|
50
|
+
.sort();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const QUERY_REPORT_NAMES = [
|
|
54
|
+
"QueryReportColumn",
|
|
55
|
+
"QueryReportRow",
|
|
56
|
+
"QueryReportFilters",
|
|
57
|
+
"QueryReportFilterMeta",
|
|
58
|
+
"QueryReportMeta",
|
|
59
|
+
"QueryReportFixture",
|
|
60
|
+
"QueryReportRunMessage",
|
|
61
|
+
"ScriptFilterExtraction",
|
|
62
|
+
"buildRunFilters",
|
|
63
|
+
"compactFilterValues",
|
|
64
|
+
"compactQueryReportFilters",
|
|
65
|
+
"defaultFilterValues",
|
|
66
|
+
"extractScriptFilters",
|
|
67
|
+
"humanizeFrappeError",
|
|
68
|
+
"isEmptyFilterValue",
|
|
69
|
+
"normalizeFilterMeta",
|
|
70
|
+
"normalizeQueryReportColumns",
|
|
71
|
+
"normalizeQueryReportPayload",
|
|
72
|
+
"normalizeQueryReportRows",
|
|
73
|
+
"scrubFieldname",
|
|
74
|
+
"stripReportHtml",
|
|
75
|
+
"useFrappeReport",
|
|
76
|
+
"useQueryReport",
|
|
77
|
+
"UseQueryReportOptions",
|
|
78
|
+
"UseQueryReportResult",
|
|
79
|
+
"buildQueryReportColumns",
|
|
80
|
+
"buildQueryReportCsv",
|
|
81
|
+
"BuildQueryReportColumnsArgs",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const REPORT_VIEW_NAMES = [
|
|
85
|
+
"ReportColumnDef",
|
|
86
|
+
"ReportRow",
|
|
87
|
+
"ReportSortState",
|
|
88
|
+
"ReportViewConfig",
|
|
89
|
+
"DeskReportSettings",
|
|
90
|
+
"ReportGridRow",
|
|
91
|
+
"ReportTotalsCell",
|
|
92
|
+
"REPORT_NUMERIC_FIELDTYPES",
|
|
93
|
+
"REPORT_PAGE_SIZES",
|
|
94
|
+
"DEFAULT_REPORT_PAGE_SIZE",
|
|
95
|
+
"reportColumnsFromMeta",
|
|
96
|
+
"defaultReportFieldnames",
|
|
97
|
+
"resolveReportColumns",
|
|
98
|
+
"reportColumnFilterTuple",
|
|
99
|
+
"reportColumnFilterTuples",
|
|
100
|
+
"parseReportFiltersParam",
|
|
101
|
+
"parseDeskOrderBy",
|
|
102
|
+
"toDeskReportSettings",
|
|
103
|
+
"fromDeskReportSettings",
|
|
104
|
+
"orderReportRows",
|
|
105
|
+
"buildReportGridRows",
|
|
106
|
+
"computeReportTotals",
|
|
107
|
+
"buildReportCsv",
|
|
108
|
+
"ReportGrid",
|
|
109
|
+
"ReportGridProps",
|
|
110
|
+
"ReportViewToolbar",
|
|
111
|
+
"ReportViewToolbarProps",
|
|
112
|
+
"useSavedReports",
|
|
113
|
+
"SavedReportSummary",
|
|
114
|
+
"UseSavedReportsResult",
|
|
115
|
+
"dialogBodyMinWidth",
|
|
116
|
+
"overlayListMaxHeight",
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
describe("report seam (MPO-15, GL-28)", () => {
|
|
120
|
+
it("hands both report models out of the package barrel", () => {
|
|
121
|
+
const barrel = read("index.ts");
|
|
122
|
+
const start = barrel.indexOf("Report models (MPO-15");
|
|
123
|
+
expect(start, "the barrel must carry a report models block").toBeGreaterThan(-1);
|
|
124
|
+
const block = barrel.slice(start);
|
|
125
|
+
expect(block).toContain('from "./report/queryReport"');
|
|
126
|
+
expect(block).toContain('from "./report/reportViewModel"');
|
|
127
|
+
for (const name of [...QUERY_REPORT_NAMES, ...REPORT_VIEW_NAMES]) {
|
|
128
|
+
expect(block, `the barrel must re-export "${name}"`).toMatch(new RegExp(`\\b${name}\\b`));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("keeps the report modules off backoffice and off their own barrel", () => {
|
|
133
|
+
const files = [
|
|
134
|
+
...sourceFiles(reportDir),
|
|
135
|
+
path.resolve(srcDir, "FrappeReportView.tsx"),
|
|
136
|
+
path.resolve(srcDir, "FrappeReportBuilder.tsx"),
|
|
137
|
+
];
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
140
|
+
expect(source, file).not.toMatch(/from ["']@multiplatform\.one\/backoffice/);
|
|
141
|
+
expect(source, file).not.toMatch(/from ["']@multiplatform\.one\/frappe-ui/);
|
|
142
|
+
expect(source, file).not.toMatch(/from ["']\.\.?\/index["']/);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("scrubs a label the way the server's frappe.scrub does", async () => {
|
|
147
|
+
const modelPath = "./queryReport";
|
|
148
|
+
const { scrubFieldname } = (await import(/* @vite-ignore */ modelPath)) as {
|
|
149
|
+
scrubFieldname: (label: string) => string;
|
|
150
|
+
};
|
|
151
|
+
expect(scrubFieldname("Modified On")).toBe("modified_on");
|
|
152
|
+
expect(scrubFieldname("Multi-Word Label")).toBe("multi_word_label");
|
|
153
|
+
expect(scrubFieldname("Size (MB)")).toBe("size_(mb)");
|
|
154
|
+
expect(scrubFieldname("Assigned To/Owner")).toBe("assigned_to/owner");
|
|
155
|
+
expect(scrubFieldname(" Total (USD) ")).toBe("__total_(usd)_");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("declares the query-report model once, across both packages", () => {
|
|
159
|
+
expect(declaredIn(/function scrubFieldname\b/)).toEqual([
|
|
160
|
+
"frappe-ui/src/report/queryReport.ts",
|
|
161
|
+
]);
|
|
162
|
+
expect(declaredIn(/function normalizeQueryReportColumns\b/)).toEqual([
|
|
163
|
+
"frappe-ui/src/report/queryReport.ts",
|
|
164
|
+
]);
|
|
165
|
+
expect(declaredIn(/function normalizeQueryReportRows\b/)).toEqual([
|
|
166
|
+
"frappe-ui/src/report/queryReport.ts",
|
|
167
|
+
]);
|
|
168
|
+
expect(declaredIn(/function (?:compactFilterValues|compactQueryReportFilters)\b/)).toEqual([
|
|
169
|
+
"frappe-ui/src/report/queryReport.ts",
|
|
170
|
+
]);
|
|
171
|
+
expect(declaredIn(/interface (?:QueryReportColumn|FrappeReportColumn)\b/)).toEqual([
|
|
172
|
+
"frappe-ui/src/report/queryReport.ts",
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("declares the report-view model once, across both packages", () => {
|
|
177
|
+
for (const name of [
|
|
178
|
+
"reportColumnsFromMeta",
|
|
179
|
+
"defaultReportFieldnames",
|
|
180
|
+
"columnFilterTuple",
|
|
181
|
+
"orderReportRows",
|
|
182
|
+
"buildReportCsv",
|
|
183
|
+
"parseReportFiltersParam",
|
|
184
|
+
]) {
|
|
185
|
+
expect(declaredIn(new RegExp(`function ${name}\\b`)), name).toEqual([
|
|
186
|
+
"frappe-ui/src/report/reportViewModel.ts",
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
expect(declaredIn(/REPORT_NUMERIC_FIELDTYPES\s*=/)).toEqual([
|
|
190
|
+
"frappe-ui/src/report/reportViewModel.ts",
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("renders FrappeReportView through the query-report model", () => {
|
|
195
|
+
const view = read("FrappeReportView.tsx");
|
|
196
|
+
expect(view).toContain('from "./report/queryReport"');
|
|
197
|
+
expect(view).toContain('from "./report/useFrappeReport"');
|
|
198
|
+
expect(view).not.toMatch(
|
|
199
|
+
/export function (?:normalizeReport|compactFilterValues|scrubFieldname)/,
|
|
200
|
+
);
|
|
201
|
+
const useQueryReport = read("report/useQueryReport.ts");
|
|
202
|
+
expect(useQueryReport, "the desk hook runs through the same transport").toContain(
|
|
203
|
+
'from "./useFrappeReport"',
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("renders FrappeReportBuilder through the report-view model", () => {
|
|
208
|
+
const builder = read("FrappeReportBuilder.tsx");
|
|
209
|
+
expect(builder).toContain('from "./report/reportViewModel"');
|
|
210
|
+
for (const call of ["reportColumnsFromMeta(", "defaultReportFieldnames(", "orderReportRows("]) {
|
|
211
|
+
expect(builder, call).toContain(call);
|
|
212
|
+
}
|
|
213
|
+
expect(builder).toContain("<ReportGrid");
|
|
214
|
+
expect(builder).not.toMatch(/<ReportBuilder\b/);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The query-report transport: one `frappe.desk.query_report.run` caller per
|
|
3
|
+
* connection, debounced filter changes, the Report's DB-defined filters via
|
|
4
|
+
* `get_script`. FrappeReportView runs on it directly and the desk's
|
|
5
|
+
* `useQueryReport` runs its reports through it, so both surfaces send the
|
|
6
|
+
* same request and read the same normalized payload (MPO-15, GL-28).
|
|
7
|
+
*/
|
|
8
|
+
import { AsyncDebouncer, CallModule, HttpClient, type AuthConfig } from "@multiplatform.one/frappe";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
10
|
+
import {
|
|
11
|
+
compactFilterValues,
|
|
12
|
+
normalizeQueryReportPayload,
|
|
13
|
+
type QueryReportFilterInput,
|
|
14
|
+
type QueryReportFilters,
|
|
15
|
+
type QueryReportPayload,
|
|
16
|
+
type QueryReportRunMessage,
|
|
17
|
+
} from "./queryReport";
|
|
18
|
+
|
|
19
|
+
/** Fixture payload or loader — renders the report without any network. */
|
|
20
|
+
export type FrappeReportFixture =
|
|
21
|
+
| QueryReportRunMessage
|
|
22
|
+
| ((args: {
|
|
23
|
+
reportName: string;
|
|
24
|
+
filters: QueryReportFilters;
|
|
25
|
+
}) => QueryReportRunMessage | Promise<QueryReportRunMessage>);
|
|
26
|
+
|
|
27
|
+
export interface FrappeReportConnection {
|
|
28
|
+
baseURL: string;
|
|
29
|
+
auth?: {
|
|
30
|
+
useToken?: boolean;
|
|
31
|
+
token?: string | (() => string | Promise<string>);
|
|
32
|
+
type?: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// One whitelisted-method caller per connection — report views sharing a
|
|
37
|
+
// bench share it (same pattern as useFrappeAggregate's DbModule cache).
|
|
38
|
+
const callCache = new Map<string, CallModule>();
|
|
39
|
+
|
|
40
|
+
/** The shared caller for a connection (report runs, Report doc reads, get_script). */
|
|
41
|
+
export function reportCall(connection: FrappeReportConnection): CallModule {
|
|
42
|
+
const key = `${connection.baseURL}:${connection.auth?.useToken ? "t" : ""}:${connection.auth?.token ?? ""}:${connection.auth?.type ?? ""}`;
|
|
43
|
+
let call = callCache.get(key);
|
|
44
|
+
if (!call) {
|
|
45
|
+
const authConfig: AuthConfig | undefined = connection.auth
|
|
46
|
+
? {
|
|
47
|
+
useToken: connection.auth.useToken,
|
|
48
|
+
token: connection.auth.token,
|
|
49
|
+
type: connection.auth.type as AuthConfig["type"],
|
|
50
|
+
}
|
|
51
|
+
: undefined;
|
|
52
|
+
call = new CallModule(new HttpClient(connection.baseURL, authConfig));
|
|
53
|
+
callCache.set(key, call);
|
|
54
|
+
}
|
|
55
|
+
return call;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_DEBOUNCE_MS = 400;
|
|
59
|
+
|
|
60
|
+
export interface UseFrappeReportOptions {
|
|
61
|
+
reportName: string;
|
|
62
|
+
/** Current filter values (already compacted values are fine too). */
|
|
63
|
+
filters?: QueryReportFilters;
|
|
64
|
+
/** Fixture payload / loader — no network is touched when set. */
|
|
65
|
+
fixture?: FrappeReportFixture;
|
|
66
|
+
/**
|
|
67
|
+
* Fetch the report's DB-defined filter definitions (`get_script`).
|
|
68
|
+
* Off when the consumer supplies `filterFields` explicitly.
|
|
69
|
+
*/
|
|
70
|
+
fetchFilterDefs?: boolean;
|
|
71
|
+
/** Bypass the prepared-report cache and execute the report now. */
|
|
72
|
+
ignorePreparedReport?: boolean;
|
|
73
|
+
/** Debounce for filter-change refetches in ms (default 400). */
|
|
74
|
+
debounceMs?: number;
|
|
75
|
+
enabled?: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface UseFrappeReportResult {
|
|
79
|
+
payload: QueryReportPayload | undefined;
|
|
80
|
+
/** DB-defined filter defs (`get_script`); undefined while unknown. */
|
|
81
|
+
scriptFilters: QueryReportFilterInput[] | undefined;
|
|
82
|
+
/** First run in flight — nothing to show yet. */
|
|
83
|
+
isLoading: boolean;
|
|
84
|
+
/** Any run in flight (filter-change / refresh refetches included). */
|
|
85
|
+
isFetching: boolean;
|
|
86
|
+
error: Error | undefined;
|
|
87
|
+
/** Immediate re-run (skips the debounce window). */
|
|
88
|
+
refresh: () => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run a named server-side report (`frappe.desk.query_report.run`) with the
|
|
93
|
+
* given filters. Filter-value changes are debounced; report changes and
|
|
94
|
+
* explicit `refresh()` run immediately.
|
|
95
|
+
*/
|
|
96
|
+
export function useFrappeReport(
|
|
97
|
+
connection: FrappeReportConnection | undefined,
|
|
98
|
+
{
|
|
99
|
+
reportName,
|
|
100
|
+
filters,
|
|
101
|
+
fixture,
|
|
102
|
+
fetchFilterDefs = false,
|
|
103
|
+
ignorePreparedReport = false,
|
|
104
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
105
|
+
enabled = true,
|
|
106
|
+
}: UseFrappeReportOptions,
|
|
107
|
+
): UseFrappeReportResult {
|
|
108
|
+
const isEnabled = enabled && !!reportName && (!!connection || !!fixture);
|
|
109
|
+
|
|
110
|
+
const [payload, setPayload] = useState<QueryReportPayload | undefined>(undefined);
|
|
111
|
+
const [scriptFilters, setScriptFilters] = useState<QueryReportFilterInput[] | undefined>(
|
|
112
|
+
undefined,
|
|
113
|
+
);
|
|
114
|
+
const [isLoading, setIsLoading] = useState(isEnabled);
|
|
115
|
+
const [isFetching, setIsFetching] = useState(false);
|
|
116
|
+
const [error, setError] = useState<Error | undefined>(undefined);
|
|
117
|
+
|
|
118
|
+
const compactedFilters = useMemo(() => compactFilterValues(filters), [filters]);
|
|
119
|
+
const filtersKey = JSON.stringify(compactedFilters);
|
|
120
|
+
const runKey = JSON.stringify([
|
|
121
|
+
connection?.baseURL,
|
|
122
|
+
!!fixture,
|
|
123
|
+
reportName,
|
|
124
|
+
filtersKey,
|
|
125
|
+
ignorePreparedReport,
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const fetchSeq = useRef(0);
|
|
129
|
+
const argsRef = useRef({ reportName, filters: compactedFilters, fixture, ignorePreparedReport });
|
|
130
|
+
argsRef.current = { reportName, filters: compactedFilters, fixture, ignorePreparedReport };
|
|
131
|
+
const connectionRef = useRef(connection);
|
|
132
|
+
connectionRef.current = connection;
|
|
133
|
+
|
|
134
|
+
const runFetch = useCallback(async () => {
|
|
135
|
+
const {
|
|
136
|
+
reportName: name,
|
|
137
|
+
filters: values,
|
|
138
|
+
fixture: fx,
|
|
139
|
+
ignorePreparedReport: skipPrepared,
|
|
140
|
+
} = argsRef.current;
|
|
141
|
+
const conn = connectionRef.current;
|
|
142
|
+
if (!name || (!conn && !fx)) return;
|
|
143
|
+
const seq = ++fetchSeq.current;
|
|
144
|
+
setIsFetching(true);
|
|
145
|
+
try {
|
|
146
|
+
let raw: QueryReportRunMessage | undefined;
|
|
147
|
+
if (fx) {
|
|
148
|
+
raw = typeof fx === "function" ? await fx({ reportName: name, filters: values }) : fx;
|
|
149
|
+
} else if (conn) {
|
|
150
|
+
const res = await reportCall(conn).post<{ message: QueryReportRunMessage }>(
|
|
151
|
+
"frappe.desk.query_report.run",
|
|
152
|
+
{
|
|
153
|
+
report_name: name,
|
|
154
|
+
filters: values,
|
|
155
|
+
ignore_prepared_report: skipPrepared ? 1 : 0,
|
|
156
|
+
// Desk parity: only let the server substitute saved custom
|
|
157
|
+
// filters when the caller sent no filter values at all.
|
|
158
|
+
are_default_filters: Object.keys(values).length ? 0 : 1,
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
raw = res.payload.message;
|
|
162
|
+
}
|
|
163
|
+
if (seq !== fetchSeq.current) return; // stale response — a newer run won
|
|
164
|
+
setPayload(normalizeQueryReportPayload(raw ?? {}));
|
|
165
|
+
setError(undefined);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (seq !== fetchSeq.current) return;
|
|
168
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
169
|
+
} finally {
|
|
170
|
+
if (seq === fetchSeq.current) {
|
|
171
|
+
setIsLoading(false);
|
|
172
|
+
setIsFetching(false);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
// Debouncer coalescing filter keystrokes into one refetch.
|
|
178
|
+
const debouncerRef = useRef<AsyncDebouncer<() => Promise<void>> | undefined>(undefined);
|
|
179
|
+
if (!debouncerRef.current) {
|
|
180
|
+
debouncerRef.current = new AsyncDebouncer(() => runFetch(), {
|
|
181
|
+
wait: debounceMs,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
debouncerRef.current?.setOptions({ wait: debounceMs });
|
|
186
|
+
}, [debounceMs]);
|
|
187
|
+
useEffect(() => () => debouncerRef.current?.cancel(), []);
|
|
188
|
+
|
|
189
|
+
// Report identity changes (and first mount) run immediately; filter-value
|
|
190
|
+
// changes ride the debouncer so per-keystroke Data filters cost one run.
|
|
191
|
+
// The run key guards actual query changes; captured args ride argsRef.
|
|
192
|
+
const lastRunKeyRef = useRef<string | undefined>(undefined);
|
|
193
|
+
const lastReportRef = useRef<string | undefined>(undefined);
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!isEnabled || lastRunKeyRef.current === runKey) return;
|
|
196
|
+
lastRunKeyRef.current = runKey;
|
|
197
|
+
const reportChanged = lastReportRef.current !== argsRef.current.reportName;
|
|
198
|
+
lastReportRef.current = argsRef.current.reportName;
|
|
199
|
+
setError(undefined);
|
|
200
|
+
if (reportChanged) {
|
|
201
|
+
setIsLoading(true);
|
|
202
|
+
debouncerRef.current?.cancel();
|
|
203
|
+
void runFetch();
|
|
204
|
+
} else if (debounceMs <= 0) {
|
|
205
|
+
void runFetch();
|
|
206
|
+
} else {
|
|
207
|
+
void debouncerRef.current?.maybeExecute();
|
|
208
|
+
}
|
|
209
|
+
}, [isEnabled, runKey, debounceMs, runFetch]);
|
|
210
|
+
|
|
211
|
+
// DB-defined filter definitions (Report doc `filters` child table) via
|
|
212
|
+
// get_script — live mode only; fixtures carry defs on the payload itself.
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!isEnabled || !fetchFilterDefs || fixture || !connection) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let cancelled = false;
|
|
218
|
+
void (async () => {
|
|
219
|
+
try {
|
|
220
|
+
const res = await reportCall(connection).get<{
|
|
221
|
+
message: { filters?: QueryReportFilterInput[] };
|
|
222
|
+
}>("frappe.desk.query_report.get_script", { report_name: reportName });
|
|
223
|
+
if (!cancelled) setScriptFilters(res.payload.message?.filters ?? []);
|
|
224
|
+
} catch {
|
|
225
|
+
// Filter defs are progressive enhancement — a failing get_script
|
|
226
|
+
// must not take the report down (run() surfaces real errors).
|
|
227
|
+
if (!cancelled) setScriptFilters([]);
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
return () => {
|
|
231
|
+
cancelled = true;
|
|
232
|
+
};
|
|
233
|
+
}, [isEnabled, fetchFilterDefs, fixture, connection, reportName]);
|
|
234
|
+
|
|
235
|
+
const refresh = useCallback(() => {
|
|
236
|
+
debouncerRef.current?.cancel();
|
|
237
|
+
void runFetch();
|
|
238
|
+
}, [runFetch]);
|
|
239
|
+
|
|
240
|
+
return { payload, scriptFilters, isLoading, isFetching, error, refresh };
|
|
241
|
+
}
|