@multiplatform.one/frappe-ui 7.28.1 → 7.29.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/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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useQueryReport's live path (MPO-15, GL-28): the desk hook reads the Report
|
|
3
|
+
* doc and script, then runs the report through useFrappeReport, the
|
|
4
|
+
* transport FrappeReportView uses, and hands back the one model's columns and
|
|
5
|
+
* rows. A refused run keeps its HTTP status on the humanized error.
|
|
6
|
+
*/
|
|
7
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
8
|
+
import React, { type ReactNode } from "react";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { FrappeProvider } from "../FrappeProvider";
|
|
11
|
+
import { useQueryReport } from "./useQueryReport";
|
|
12
|
+
|
|
13
|
+
interface Recorded {
|
|
14
|
+
method: string;
|
|
15
|
+
url: string;
|
|
16
|
+
body?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function json(body: unknown, status = 200): Response {
|
|
20
|
+
return new Response(JSON.stringify(body), {
|
|
21
|
+
status,
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let calls: Recorded[] = [];
|
|
27
|
+
let runResponse: () => Response;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
calls = [];
|
|
31
|
+
runResponse = () =>
|
|
32
|
+
json({
|
|
33
|
+
message: {
|
|
34
|
+
columns: ["ID:Link/ToDo:120", { label: "Assigned To/Owner", fieldtype: "Data" }],
|
|
35
|
+
result: [
|
|
36
|
+
["TD-1", "jane@example.com"],
|
|
37
|
+
{ id: "TD-2", "assigned_to/owner": "joe@example.com" },
|
|
38
|
+
],
|
|
39
|
+
execution_time: 0.25,
|
|
40
|
+
message: "Two open",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
vi.stubGlobal(
|
|
44
|
+
"fetch",
|
|
45
|
+
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
46
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
47
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
48
|
+
const body =
|
|
49
|
+
typeof init?.body === "string"
|
|
50
|
+
? (JSON.parse(init.body) as Record<string, unknown>)
|
|
51
|
+
: undefined;
|
|
52
|
+
calls.push({ method, url, body });
|
|
53
|
+
if (url.includes("frappe.client.get")) {
|
|
54
|
+
return json({
|
|
55
|
+
message: {
|
|
56
|
+
name: "Open Work",
|
|
57
|
+
report_name: "Open Work",
|
|
58
|
+
filters: [{ fieldname: "owner", label: "Owner", fieldtype: "Data", default: "jane" }],
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (url.includes("frappe.desk.query_report.get_script")) return json({ message: {} });
|
|
63
|
+
if (url.includes("frappe.desk.query_report.run")) return runResponse();
|
|
64
|
+
return json({ message: null });
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
vi.unstubAllGlobals();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
let run = 0;
|
|
74
|
+
function wrapper() {
|
|
75
|
+
run += 1;
|
|
76
|
+
const baseURL = `https://query-report-${run}.spec.test`;
|
|
77
|
+
return ({ children }: { children: ReactNode }) => (
|
|
78
|
+
<FrappeProvider baseURL={baseURL} realtime={false}>
|
|
79
|
+
{children}
|
|
80
|
+
</FrappeProvider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("useQueryReport over the shared transport", () => {
|
|
85
|
+
it("seeds the doc's defaults, runs once, and reads the model's rows", async () => {
|
|
86
|
+
const { result } = renderHook(() => useQueryReport("Open Work"), { wrapper: wrapper() });
|
|
87
|
+
await waitFor(() => expect(result.current.loading).toBe(false), { timeout: 5000 });
|
|
88
|
+
expect(result.current.error).toBeUndefined();
|
|
89
|
+
expect(result.current.columns.map((column) => column.fieldname)).toEqual([
|
|
90
|
+
"id",
|
|
91
|
+
"assigned_to/owner",
|
|
92
|
+
]);
|
|
93
|
+
expect(result.current.rows).toEqual([
|
|
94
|
+
{ id: "TD-1", "assigned_to/owner": "jane@example.com" },
|
|
95
|
+
{ id: "TD-2", "assigned_to/owner": "joe@example.com" },
|
|
96
|
+
]);
|
|
97
|
+
expect(result.current.executionTime).toBe(0.25);
|
|
98
|
+
expect(result.current.serverMessage).toBe("Two open");
|
|
99
|
+
|
|
100
|
+
const runs = calls.filter((call) => call.url.includes("frappe.desk.query_report.run"));
|
|
101
|
+
expect(runs).toHaveLength(1);
|
|
102
|
+
expect(runs[0].method).toBe("POST");
|
|
103
|
+
expect(runs[0].body).toMatchObject({
|
|
104
|
+
report_name: "Open Work",
|
|
105
|
+
filters: { owner: "jane" },
|
|
106
|
+
ignore_prepared_report: 1,
|
|
107
|
+
are_default_filters: 0,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("keeps the refused run's status on the humanized error", async () => {
|
|
112
|
+
runResponse = () =>
|
|
113
|
+
json(
|
|
114
|
+
{
|
|
115
|
+
exc_type: "PermissionError",
|
|
116
|
+
_server_messages: JSON.stringify([
|
|
117
|
+
JSON.stringify({ message: "You are not permitted to run Open Work" }),
|
|
118
|
+
]),
|
|
119
|
+
},
|
|
120
|
+
403,
|
|
121
|
+
);
|
|
122
|
+
const { result } = renderHook(() => useQueryReport("Open Work"), { wrapper: wrapper() });
|
|
123
|
+
await waitFor(() => expect(result.current.error).toBeDefined(), { timeout: 5000 });
|
|
124
|
+
const error = result.current.error as Error & { status?: number; exc_type?: string };
|
|
125
|
+
expect(error.message).toBe("You are not permitted to run Open Work");
|
|
126
|
+
expect(error.status).toBe(403);
|
|
127
|
+
expect(error.exc_type).toBe("PermissionError");
|
|
128
|
+
expect(result.current.rows).toEqual([]);
|
|
129
|
+
expect(result.current.loading).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Data hook for frappe Query / Script Reports (the desk
|
|
2
|
+
// /app/query-report/<name> surface): loads the Report doc (title,
|
|
3
|
+
// ref_doctype, doc-level filters) plus the desk-side script module
|
|
4
|
+
// (frappe.desk.query_report.get_script) and runs the report through
|
|
5
|
+
// useFrappeReport, the transport FrappeReportView uses too. Wire-shape
|
|
6
|
+
// normalization (and the probed response documentation) lives in
|
|
7
|
+
// queryReport.ts; this file owns the desk's filter state and gating.
|
|
8
|
+
//
|
|
9
|
+
// GET /api/method/frappe.client.get {doctype:"Report", name}
|
|
10
|
+
// → {message: {report_name, ref_doctype, report_type, disabled,
|
|
11
|
+
// prepared_report, add_total_row, filters: [...], ...}}
|
|
12
|
+
// GET /api/method/frappe.desk.query_report.get_script {report_name}
|
|
13
|
+
// → {message: {script, html_format, execution_time, filters}}
|
|
14
|
+
//
|
|
15
|
+
// Filter meta sources (desk precedence, query_report.js setup): the script
|
|
16
|
+
// module's declarative `filters` win when the script declares any; the
|
|
17
|
+
// Report doc `filters` child table applies only when it doesn't. Script
|
|
18
|
+
// filters are extracted from the raw JS text without eval
|
|
19
|
+
// (queryReport.extractScriptFilters) — entries beyond the declarative
|
|
20
|
+
// subset (get_query functions, dynamic defaults, desk-only fieldtypes)
|
|
21
|
+
// surface as `scriptFilterGap` so the screen can render an honest note.
|
|
22
|
+
// `ignore_prepared_report` forces a synchronous run for prepared reports
|
|
23
|
+
// (desk's background Prepared Report queue is out of scope here).
|
|
24
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
25
|
+
import { useFrappeConfig } from "../FrappeProvider";
|
|
26
|
+
import { isPermissionError } from "../liveErrors";
|
|
27
|
+
import {
|
|
28
|
+
type QueryReportColumn,
|
|
29
|
+
type QueryReportFilterMeta,
|
|
30
|
+
type QueryReportFilters,
|
|
31
|
+
type QueryReportFixture,
|
|
32
|
+
type QueryReportMeta,
|
|
33
|
+
type QueryReportRow,
|
|
34
|
+
buildRunFilters,
|
|
35
|
+
defaultFilterValues,
|
|
36
|
+
extractScriptFilters,
|
|
37
|
+
humanizeFrappeError,
|
|
38
|
+
isEmptyFilterValue,
|
|
39
|
+
normalizeFilterMeta,
|
|
40
|
+
normalizeQueryReportColumns,
|
|
41
|
+
normalizeQueryReportRows,
|
|
42
|
+
} from "./queryReport";
|
|
43
|
+
import { reportCall, useFrappeReport, type FrappeReportConnection } from "./useFrappeReport";
|
|
44
|
+
|
|
45
|
+
const SCRIPT_METHOD = "frappe.desk.query_report.get_script";
|
|
46
|
+
const EMPTY_COLUMNS: QueryReportColumn[] = [];
|
|
47
|
+
const EMPTY_ROWS: QueryReportRow[] = [];
|
|
48
|
+
|
|
49
|
+
export interface UseQueryReportOptions {
|
|
50
|
+
fixture?: QueryReportFixture;
|
|
51
|
+
/** Filter values applied before the first run (report swaps re-apply them). */
|
|
52
|
+
initialFilters?: QueryReportFilters;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseQueryReportResult {
|
|
56
|
+
meta?: QueryReportMeta;
|
|
57
|
+
columns: QueryReportColumn[];
|
|
58
|
+
rows: QueryReportRow[];
|
|
59
|
+
/** True until the first meta+script+run cycle settles. */
|
|
60
|
+
loading: boolean;
|
|
61
|
+
/** True while a (re)run is in flight. */
|
|
62
|
+
running: boolean;
|
|
63
|
+
/** The failed run's server sentence; `status` and `exc_type` ride along. */
|
|
64
|
+
error?: Error;
|
|
65
|
+
/** The server refused the run: the session may not run this report (LC-96). */
|
|
66
|
+
refused: boolean;
|
|
67
|
+
/** The report's informational `message` payload (may embed HTML). */
|
|
68
|
+
serverMessage?: string;
|
|
69
|
+
/** Server-reported run time in seconds. */
|
|
70
|
+
executionTime?: number;
|
|
71
|
+
/** Merged filter meta: script-declared filters, else doc-level rows. */
|
|
72
|
+
filterMeta: QueryReportFilterMeta[];
|
|
73
|
+
/**
|
|
74
|
+
* True when the desk script declares filters this surface could not
|
|
75
|
+
* derive (non-declarative entries) — the honest "defined in code" note.
|
|
76
|
+
*/
|
|
77
|
+
scriptFilterGap: boolean;
|
|
78
|
+
/** Mandatory filters currently unfilled — the run is skipped while non-empty. */
|
|
79
|
+
missingRequired: QueryReportFilterMeta[];
|
|
80
|
+
filters: QueryReportFilters;
|
|
81
|
+
setFilters: (next: QueryReportFilters) => void;
|
|
82
|
+
reload: () => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function fetchReportMeta(
|
|
86
|
+
connection: FrappeReportConnection,
|
|
87
|
+
reportName: string,
|
|
88
|
+
): Promise<QueryReportMeta> {
|
|
89
|
+
const res = await reportCall(connection).get<{ message?: QueryReportMeta }>("frappe.client.get", {
|
|
90
|
+
doctype: "Report",
|
|
91
|
+
name: reportName,
|
|
92
|
+
});
|
|
93
|
+
const doc = res.payload?.message;
|
|
94
|
+
if (!doc) throw new Error(`Report "${reportName}" not found`);
|
|
95
|
+
return doc;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function fetchReportScript(
|
|
99
|
+
connection: FrappeReportConnection,
|
|
100
|
+
reportName: string,
|
|
101
|
+
): Promise<string | undefined> {
|
|
102
|
+
const res = await reportCall(connection).get<{ message?: { script?: string | null } }>(
|
|
103
|
+
SCRIPT_METHOD,
|
|
104
|
+
{ report_name: reportName },
|
|
105
|
+
);
|
|
106
|
+
const script = res.payload?.message?.script;
|
|
107
|
+
return typeof script === "string" ? script : undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function useQueryReport(
|
|
111
|
+
reportName: string,
|
|
112
|
+
options: UseQueryReportOptions = {},
|
|
113
|
+
): UseQueryReportResult {
|
|
114
|
+
const { fixture } = options;
|
|
115
|
+
const config = useFrappeConfig();
|
|
116
|
+
const baseURL = config?.baseURL;
|
|
117
|
+
const auth = config?.auth;
|
|
118
|
+
const connection = useMemo<FrappeReportConnection | undefined>(
|
|
119
|
+
() => (baseURL ? { baseURL, auth } : undefined),
|
|
120
|
+
[baseURL, auth],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Latest initialFilters without re-running effects on identity churn.
|
|
124
|
+
const initialFiltersRef = useRef(options.initialFilters);
|
|
125
|
+
initialFiltersRef.current = options.initialFilters;
|
|
126
|
+
|
|
127
|
+
const [meta, setMeta] = useState<QueryReportMeta | undefined>(undefined);
|
|
128
|
+
const [script, setScript] = useState<string | undefined>(undefined);
|
|
129
|
+
const [sourcesSettled, setSourcesSettled] = useState(false);
|
|
130
|
+
const [filters, setFiltersState] = useState<QueryReportFilters>(() => ({
|
|
131
|
+
...initialFiltersRef.current,
|
|
132
|
+
}));
|
|
133
|
+
const seededFor = useRef<string | undefined>(undefined);
|
|
134
|
+
|
|
135
|
+
// The route component is reused across query-report navigations — a
|
|
136
|
+
// report swap resets filters and shows the loading state again.
|
|
137
|
+
const lastReport = useRef(reportName);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (lastReport.current === reportName) return;
|
|
140
|
+
lastReport.current = reportName;
|
|
141
|
+
setFiltersState({ ...initialFiltersRef.current });
|
|
142
|
+
setMeta(undefined);
|
|
143
|
+
setScript(undefined);
|
|
144
|
+
setSourcesSettled(false);
|
|
145
|
+
}, [reportName]);
|
|
146
|
+
|
|
147
|
+
// Report doc meta (title, ref_doctype, doc filters) + the desk script
|
|
148
|
+
// module, fetched together. Both are non-fatal niceties — the run below
|
|
149
|
+
// reports the real errors — but the run waits for them so declared
|
|
150
|
+
// defaults are seeded into the FIRST run (desk parity).
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (fixture) return;
|
|
153
|
+
if (!connection) {
|
|
154
|
+
setSourcesSettled(true);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let cancelled = false;
|
|
158
|
+
setSourcesSettled(false);
|
|
159
|
+
Promise.allSettled([
|
|
160
|
+
fetchReportMeta(connection, reportName),
|
|
161
|
+
fetchReportScript(connection, reportName),
|
|
162
|
+
]).then(([metaResult, scriptResult]) => {
|
|
163
|
+
if (cancelled) return;
|
|
164
|
+
const doc = metaResult.status === "fulfilled" ? metaResult.value : undefined;
|
|
165
|
+
const scriptText = scriptResult.status === "fulfilled" ? scriptResult.value : undefined;
|
|
166
|
+
if (doc) setMeta(doc);
|
|
167
|
+
if (scriptText) setScript(scriptText);
|
|
168
|
+
// Seed filter defaults once per report; user edits (and deep-link
|
|
169
|
+
// initialFilters, already in state) win over defaults.
|
|
170
|
+
if (seededFor.current !== reportName) {
|
|
171
|
+
seededFor.current = reportName;
|
|
172
|
+
const extraction = extractScriptFilters(scriptText);
|
|
173
|
+
const merged = extraction.declared ? extraction.filters : normalizeFilterMeta(doc?.filters);
|
|
174
|
+
const defaults = defaultFilterValues(merged);
|
|
175
|
+
if (Object.keys(defaults).length > 0)
|
|
176
|
+
setFiltersState((previous) => ({ ...defaults, ...previous }));
|
|
177
|
+
}
|
|
178
|
+
setSourcesSettled(true);
|
|
179
|
+
});
|
|
180
|
+
return () => {
|
|
181
|
+
cancelled = true;
|
|
182
|
+
};
|
|
183
|
+
}, [connection, fixture, reportName]);
|
|
184
|
+
|
|
185
|
+
const scriptExtraction = useMemo(
|
|
186
|
+
() => extractScriptFilters(fixture ? fixture.script : script),
|
|
187
|
+
[fixture, script],
|
|
188
|
+
);
|
|
189
|
+
const docFilterMeta = useMemo(
|
|
190
|
+
() => normalizeFilterMeta(fixture ? fixture.meta?.filters : meta?.filters),
|
|
191
|
+
[fixture, meta],
|
|
192
|
+
);
|
|
193
|
+
// Desk precedence (query_report.js): the script module's filters apply
|
|
194
|
+
// when the script declares the key at all; doc rows only otherwise.
|
|
195
|
+
const filterMeta = useMemo(
|
|
196
|
+
() => (scriptExtraction.declared ? scriptExtraction.filters : docFilterMeta),
|
|
197
|
+
[scriptExtraction, docFilterMeta],
|
|
198
|
+
);
|
|
199
|
+
const scriptFilterGap = scriptExtraction.declaredCount > scriptExtraction.filters.length;
|
|
200
|
+
|
|
201
|
+
// Fixture branch seeds defaults too (no fetch effect runs for fixtures).
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!fixture) return;
|
|
204
|
+
if (seededFor.current === reportName) return;
|
|
205
|
+
seededFor.current = reportName;
|
|
206
|
+
const defaults = defaultFilterValues(filterMeta);
|
|
207
|
+
if (Object.keys(defaults).length > 0)
|
|
208
|
+
setFiltersState((previous) => ({ ...defaults, ...previous }));
|
|
209
|
+
}, [fixture, reportName, filterMeta]);
|
|
210
|
+
|
|
211
|
+
// Desk parity: defaults SEED the controls (state) once; after that the
|
|
212
|
+
// gate reads the live values only — clearing a defaulted mandatory filter
|
|
213
|
+
// re-gates instead of firing a doomed run (desk's get_filter_values reads
|
|
214
|
+
// control values, not declarations).
|
|
215
|
+
const missingRequired = useMemo(
|
|
216
|
+
() =>
|
|
217
|
+
filterMeta.filter(
|
|
218
|
+
(filter) => filter.mandatory === 1 && isEmptyFilterValue(filters[filter.fieldname]),
|
|
219
|
+
),
|
|
220
|
+
[filterMeta, filters],
|
|
221
|
+
);
|
|
222
|
+
const hasMissingRequired = missingRequired.length > 0;
|
|
223
|
+
const metaDisabled = (fixture ? fixture.meta?.disabled : meta?.disabled) === 1;
|
|
224
|
+
|
|
225
|
+
const runFilters = useMemo(() => buildRunFilters(filterMeta, filters), [filterMeta, filters]);
|
|
226
|
+
const runEnabled =
|
|
227
|
+
!fixture && sourcesSettled && Boolean(connection) && !metaDisabled && !hasMissingRequired;
|
|
228
|
+
const run = useFrappeReport(connection, {
|
|
229
|
+
reportName,
|
|
230
|
+
filters: runFilters,
|
|
231
|
+
ignorePreparedReport: true,
|
|
232
|
+
debounceMs: 0,
|
|
233
|
+
enabled: runEnabled,
|
|
234
|
+
});
|
|
235
|
+
const { refresh } = run;
|
|
236
|
+
|
|
237
|
+
const setFilters = useCallback((next: QueryReportFilters) => {
|
|
238
|
+
setFiltersState(next);
|
|
239
|
+
}, []);
|
|
240
|
+
|
|
241
|
+
const reload = useCallback(() => {
|
|
242
|
+
if (runEnabled) refresh();
|
|
243
|
+
}, [runEnabled, refresh]);
|
|
244
|
+
|
|
245
|
+
const runError = useMemo(
|
|
246
|
+
() => (run.error ? humanizeFrappeError(run.error) : undefined),
|
|
247
|
+
[run.error],
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Fixture branch: static data, no RPCs (stories/tests).
|
|
251
|
+
const fixtureResult = useMemo(() => {
|
|
252
|
+
if (!fixture) return undefined;
|
|
253
|
+
const fixtureColumns = normalizeQueryReportColumns(fixture.columns);
|
|
254
|
+
return {
|
|
255
|
+
columns: fixtureColumns,
|
|
256
|
+
rows: normalizeQueryReportRows(fixture.rows, fixtureColumns),
|
|
257
|
+
meta: fixture.meta ? ({ name: reportName, ...fixture.meta } as QueryReportMeta) : undefined,
|
|
258
|
+
};
|
|
259
|
+
}, [fixture, reportName]);
|
|
260
|
+
|
|
261
|
+
if (fixtureResult) {
|
|
262
|
+
return {
|
|
263
|
+
meta: fixtureResult.meta,
|
|
264
|
+
columns: fixtureResult.columns,
|
|
265
|
+
rows: hasMissingRequired ? EMPTY_ROWS : fixtureResult.rows,
|
|
266
|
+
loading: false,
|
|
267
|
+
running: false,
|
|
268
|
+
error: undefined,
|
|
269
|
+
refused: false,
|
|
270
|
+
serverMessage: fixture?.message,
|
|
271
|
+
executionTime: undefined,
|
|
272
|
+
filterMeta,
|
|
273
|
+
scriptFilterGap,
|
|
274
|
+
missingRequired,
|
|
275
|
+
filters,
|
|
276
|
+
setFilters,
|
|
277
|
+
reload,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const noConnection = sourcesSettled && !connection;
|
|
282
|
+
// A settled run shows its result; a gated one (disabled report, missing
|
|
283
|
+
// mandatory filters) shows nothing rather than the last report's rows.
|
|
284
|
+
const showsRun = runEnabled && !runError;
|
|
285
|
+
const payload = showsRun ? run.payload : undefined;
|
|
286
|
+
const error = noConnection
|
|
287
|
+
? new Error("No Frappe connection configured")
|
|
288
|
+
: runEnabled
|
|
289
|
+
? runError
|
|
290
|
+
: undefined;
|
|
291
|
+
return {
|
|
292
|
+
meta,
|
|
293
|
+
columns: payload?.columns ?? EMPTY_COLUMNS,
|
|
294
|
+
rows: payload?.rows ?? EMPTY_ROWS,
|
|
295
|
+
loading: !sourcesSettled || (runEnabled && (run.isLoading || (!run.payload && !run.error))),
|
|
296
|
+
running: runEnabled && run.isFetching,
|
|
297
|
+
error,
|
|
298
|
+
refused: error !== undefined && isPermissionError(error),
|
|
299
|
+
serverMessage: payload?.message,
|
|
300
|
+
executionTime:
|
|
301
|
+
typeof payload?.raw.execution_time === "number" ? payload.raw.execution_time : undefined,
|
|
302
|
+
filterMeta,
|
|
303
|
+
scriptFilterGap,
|
|
304
|
+
missingRequired,
|
|
305
|
+
filters,
|
|
306
|
+
setFilters,
|
|
307
|
+
reload,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFrappeForm is FrappeForm's own state, handed to its caller (MPO-15
|
|
3
|
+
* AC2): a page renders the fields with `<FrappeForm form={controller} />`
|
|
4
|
+
* and builds its chrome on the same controller, so the desk and a bare
|
|
5
|
+
* FrappeForm run one engine. A new record also starts from the doctype's
|
|
6
|
+
* defaults, the way desk create_new seeds it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { InMemoryFixtureProvider } from "@multiplatform.one/frappe";
|
|
10
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
11
|
+
import { act, cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
|
12
|
+
import i18next from "i18next";
|
|
13
|
+
import { initReactI18next } from "react-i18next";
|
|
14
|
+
import { Text } from "tamagui";
|
|
15
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
|
+
import { assetLoanLifecycleFixtures } from "../tests/fixtures/assetLoanLifecycle";
|
|
17
|
+
import { setCreatePrefill } from "./formEngine/createPrefill";
|
|
18
|
+
import { FrappeForm } from "./FrappeForm";
|
|
19
|
+
import { FrappeProvider } from "./FrappeProvider";
|
|
20
|
+
import type { FieldOverride } from "./schemaForm";
|
|
21
|
+
import { useFrappeForm, type FrappeFormController } from "./useFrappeForm";
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
await i18next.use(initReactI18next).init({
|
|
25
|
+
lng: "en",
|
|
26
|
+
interpolation: { escapeValue: false },
|
|
27
|
+
resources: { en: { translation: {} } },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let confirm: ReturnType<typeof vi.fn>;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
confirm = vi.fn(() => true);
|
|
35
|
+
vi.stubGlobal("confirm", confirm);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
cleanup();
|
|
40
|
+
vi.unstubAllGlobals();
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
type Doc = Record<string, unknown> & { name: string; doctype: string };
|
|
45
|
+
|
|
46
|
+
const probe: FieldOverride = (props) => {
|
|
47
|
+
const name = props.fieldMeta?.name ?? "";
|
|
48
|
+
const value = props.value === undefined || props.value === null ? "" : String(props.value);
|
|
49
|
+
return <Text testID={`probe-${name}`}>{`val=${value} err=${props.error ?? ""}`}</Text>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ticketMeta = {
|
|
53
|
+
name: "Desk Ticket",
|
|
54
|
+
doctype: "DocType",
|
|
55
|
+
autoname: "hash",
|
|
56
|
+
fields: [
|
|
57
|
+
{ fieldname: "subject", fieldtype: "Data", label: "Subject", reqd: 1 },
|
|
58
|
+
{ fieldname: "status", fieldtype: "Select", label: "Status", options: "Open\nClosed" },
|
|
59
|
+
{ fieldname: "priority", fieldtype: "Data", label: "Priority", default: "Medium" },
|
|
60
|
+
{ fieldname: "more_tab", fieldtype: "Tab Break", label: "More" },
|
|
61
|
+
{ fieldname: "resolution", fieldtype: "Data", label: "Resolution", reqd: 1 },
|
|
62
|
+
],
|
|
63
|
+
permissions: [{ role: "All", read: 1, write: 1, create: 1 }],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const ticketOverrides = { subject: probe, status: probe, priority: probe, resolution: probe };
|
|
67
|
+
|
|
68
|
+
function ticketFixtures() {
|
|
69
|
+
return new InMemoryFixtureProvider({
|
|
70
|
+
DocType: [ticketMeta],
|
|
71
|
+
"Desk Ticket": [
|
|
72
|
+
{ name: "TICKET-1", doctype: "Desk Ticket", subject: "Printer", status: "Open" },
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let controller: FrappeFormController<Doc> | undefined;
|
|
78
|
+
|
|
79
|
+
function Host({ doctype, name }: { doctype: string; name?: string }) {
|
|
80
|
+
const form = useFrappeForm<Doc>({ doctype, name, roles: ["Loan Manager", "All"] });
|
|
81
|
+
controller = form;
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
<button type="button" data-testid="host-validate" onClick={() => revealFirst(form)}>
|
|
85
|
+
validate
|
|
86
|
+
</button>
|
|
87
|
+
<button type="button" data-testid="host-submit" onClick={() => void form.submitDocument()}>
|
|
88
|
+
submit
|
|
89
|
+
</button>
|
|
90
|
+
<FrappeForm<Doc>
|
|
91
|
+
form={form}
|
|
92
|
+
doctype={doctype}
|
|
93
|
+
name={name}
|
|
94
|
+
overrides={{ ...ticketOverrides, employee_name: probe }}
|
|
95
|
+
showToolbar={false}
|
|
96
|
+
showSidebar={false}
|
|
97
|
+
showTimeline={false}
|
|
98
|
+
showBanners={false}
|
|
99
|
+
/>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function revealFirst(form: FrappeFormController<Doc>) {
|
|
105
|
+
const errors = form.validate();
|
|
106
|
+
form.setErrors(errors);
|
|
107
|
+
Object.keys(errors).some((field) => form.revealField(field));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function expectProbe(field: string, text: string): Promise<void> {
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
expect(screen.getByTestId(`probe-${field}`).textContent).toContain(text);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe("FrappeForm seeds a new record from the doctype's defaults", () => {
|
|
117
|
+
it("a Data default and a Select's first option fill in", async () => {
|
|
118
|
+
renderWithProviders(
|
|
119
|
+
<FrappeProvider baseURL="http://test" fixtures={ticketFixtures()}>
|
|
120
|
+
<FrappeForm doctype="Desk Ticket" overrides={ticketOverrides} showSidebar={false} />
|
|
121
|
+
</FrappeProvider>,
|
|
122
|
+
);
|
|
123
|
+
await expectProbe("status", "val=Open");
|
|
124
|
+
await expectProbe("priority", "val=Medium");
|
|
125
|
+
await expectProbe("subject", "val=");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("a stashed draft (?d=, Duplicate, Amend) wins over the defaults", async () => {
|
|
129
|
+
setCreatePrefill("Desk Ticket", { priority: "High", subject: "From the list" });
|
|
130
|
+
renderWithProviders(
|
|
131
|
+
<FrappeProvider baseURL="http://test" fixtures={ticketFixtures()}>
|
|
132
|
+
<FrappeForm doctype="Desk Ticket" overrides={ticketOverrides} showSidebar={false} />
|
|
133
|
+
</FrappeProvider>,
|
|
134
|
+
);
|
|
135
|
+
await expectProbe("priority", "val=High");
|
|
136
|
+
await expectProbe("subject", "val=From the list");
|
|
137
|
+
await expectProbe("status", "val=Open");
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("a caller-owned FrappeForm", () => {
|
|
142
|
+
it("renders the fields without its own toolbar and lays out the same tabs", async () => {
|
|
143
|
+
renderWithProviders(
|
|
144
|
+
<FrappeProvider baseURL="http://test" fixtures={ticketFixtures()}>
|
|
145
|
+
<Host doctype="Desk Ticket" name="TICKET-1" />
|
|
146
|
+
</FrappeProvider>,
|
|
147
|
+
);
|
|
148
|
+
await expectProbe("subject", "val=Printer");
|
|
149
|
+
expect(screen.queryByText("Save")).toBeNull();
|
|
150
|
+
expect(controller?.fieldTargets.map((field) => [field.name, field.tabKey])).toEqual([
|
|
151
|
+
["subject", "__details"],
|
|
152
|
+
["status", "__details"],
|
|
153
|
+
["priority", "__details"],
|
|
154
|
+
["resolution", "more_tab"],
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("reveals the tab a failing required field sits on", async () => {
|
|
159
|
+
renderWithProviders(
|
|
160
|
+
<FrappeProvider baseURL="http://test" fixtures={ticketFixtures()}>
|
|
161
|
+
<Host doctype="Desk Ticket" name="TICKET-1" />
|
|
162
|
+
</FrappeProvider>,
|
|
163
|
+
);
|
|
164
|
+
await expectProbe("subject", "val=Printer");
|
|
165
|
+
expect(screen.queryByTestId("probe-resolution")).toBeNull();
|
|
166
|
+
fireEvent.click(screen.getByTestId("host-validate"));
|
|
167
|
+
await expectProbe("resolution", "err=Resolution is required");
|
|
168
|
+
expect(controller?.activeTab).toBe("more_tab");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("submits without FrappeForm's own confirm, so the host owns the question", async () => {
|
|
172
|
+
const fixtures = assetLoanLifecycleFixtures();
|
|
173
|
+
renderWithProviders(
|
|
174
|
+
<FrappeProvider baseURL="http://test" fixtures={fixtures}>
|
|
175
|
+
<Host doctype="Asset Loan" name="LOAN-DRAFT" />
|
|
176
|
+
</FrappeProvider>,
|
|
177
|
+
);
|
|
178
|
+
await expectProbe("employee_name", "val=Ada Lovelace");
|
|
179
|
+
await waitFor(() => expect(controller?.rights.submit).toBe(true));
|
|
180
|
+
fireEvent.click(screen.getByTestId("host-submit"));
|
|
181
|
+
await waitFor(async () =>
|
|
182
|
+
expect((await fixtures.getDoc("Asset Loan", "LOAN-DRAFT"))?.docstatus).toBe(1),
|
|
183
|
+
);
|
|
184
|
+
await waitFor(() => expect(controller?.docstatus).toBe(1));
|
|
185
|
+
expect(confirm).not.toHaveBeenCalled();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("save resolves with the created record's name", async () => {
|
|
189
|
+
const fixtures = ticketFixtures();
|
|
190
|
+
renderWithProviders(
|
|
191
|
+
<FrappeProvider baseURL="http://test" fixtures={fixtures}>
|
|
192
|
+
<Host doctype="Desk Ticket" />
|
|
193
|
+
</FrappeProvider>,
|
|
194
|
+
);
|
|
195
|
+
await expectProbe("priority", "val=Medium");
|
|
196
|
+
act(() => {
|
|
197
|
+
controller?.setValue("subject", "Scanner");
|
|
198
|
+
controller?.setValue("resolution", "Rebooted");
|
|
199
|
+
});
|
|
200
|
+
await waitFor(() => expect(controller?.doc.subject).toBe("Scanner"));
|
|
201
|
+
let saved: string | undefined;
|
|
202
|
+
await act(async () => {
|
|
203
|
+
saved = await controller?.save();
|
|
204
|
+
});
|
|
205
|
+
expect(saved).toBeTruthy();
|
|
206
|
+
const created = await fixtures.getDoc("Desk Ticket", String(saved));
|
|
207
|
+
expect(created).toMatchObject({ subject: "Scanner", priority: "Medium", status: "Open" });
|
|
208
|
+
});
|
|
209
|
+
});
|