@multiplatform.one/frappe-ui 7.6.3 → 7.7.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/ChartCard.cjs +52 -0
- package/dist/cjs/ChartCard.native.js +72 -0
- package/dist/cjs/ChartCard.native.js.map +1 -0
- package/dist/cjs/FileBrowser/AttachLibrary.cjs +2 -2
- package/dist/cjs/FileBrowser/AttachLibrary.native.js +8 -2
- package/dist/cjs/FileBrowser/AttachLibrary.native.js.map +1 -1
- package/dist/cjs/FrappeChartCard.cjs +3 -3
- package/dist/cjs/FrappeChartCard.native.js +3 -3
- package/dist/cjs/FrappeChartCard.native.js.map +1 -1
- package/dist/cjs/FrappeDashboard.cjs +6 -27
- package/dist/cjs/FrappeDashboard.native.js +6 -33
- package/dist/cjs/FrappeDashboard.native.js.map +1 -1
- package/dist/cjs/FrappeImageView.cjs +2 -8
- package/dist/cjs/FrappeImageView.native.js +2 -9
- package/dist/cjs/FrappeImageView.native.js.map +1 -1
- package/dist/cjs/fields/CodeEditor/index.cjs +4 -0
- package/dist/cjs/fields/CodeEditor/index.native.js +4 -0
- package/dist/cjs/fields/CodeEditor/index.native.js.map +1 -1
- package/dist/cjs/fields/LinkTree.cjs +5 -3
- package/dist/cjs/fields/LinkTree.native.js +9 -5
- package/dist/cjs/fields/LinkTree.native.js.map +1 -1
- package/dist/cjs/formEngine/createPrefill.cjs +37 -0
- package/dist/cjs/formEngine/createPrefill.native.js +40 -0
- package/dist/cjs/formEngine/createPrefill.native.js.map +1 -0
- package/dist/cjs/formEngine/dirtyTracking.cjs +55 -0
- package/dist/cjs/formEngine/dirtyTracking.native.js +77 -0
- package/dist/cjs/formEngine/dirtyTracking.native.js.map +1 -0
- package/dist/cjs/formEngine/doctypeMeta.cjs +18 -0
- package/dist/cjs/formEngine/doctypeMeta.native.js +21 -0
- package/dist/cjs/formEngine/doctypeMeta.native.js.map +1 -0
- package/dist/cjs/formEngine/fetchFrom.cjs +67 -0
- package/dist/cjs/formEngine/fetchFrom.native.js +110 -0
- package/dist/cjs/formEngine/fetchFrom.native.js.map +1 -0
- package/dist/cjs/formEngine/fieldPermissions.cjs +55 -0
- package/dist/cjs/formEngine/fieldPermissions.native.js +80 -0
- package/dist/cjs/formEngine/fieldPermissions.native.js.map +1 -0
- package/dist/cjs/formEngine/formActions.cjs +189 -0
- package/dist/cjs/formEngine/formActions.native.js +199 -0
- package/dist/cjs/formEngine/formActions.native.js.map +1 -0
- package/dist/cjs/formEngine/formLayout.cjs +245 -0
- package/dist/cjs/formEngine/formLayout.native.js +388 -0
- package/dist/cjs/formEngine/formLayout.native.js.map +1 -0
- package/dist/cjs/formEngine/index.cjs +48 -0
- package/dist/cjs/formEngine/index.native.js +51 -0
- package/dist/cjs/formEngine/index.native.js.map +1 -0
- package/dist/cjs/formEngine/useFormEngine.cjs +387 -0
- package/dist/cjs/formEngine/useFormEngine.native.js +476 -0
- package/dist/cjs/formEngine/useFormEngine.native.js.map +1 -0
- package/dist/cjs/frappeStoryTarget.cjs +4 -2
- package/dist/cjs/frappeStoryTarget.native.js +4 -3
- package/dist/cjs/frappeStoryTarget.native.js.map +1 -1
- package/dist/cjs/index.cjs +19 -1
- package/dist/cjs/index.native.js +19 -1
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/ChartCard.mjs +27 -0
- package/dist/esm/ChartCard.mjs.map +1 -0
- package/dist/esm/ChartCard.native.js +33 -0
- package/dist/esm/ChartCard.native.js.map +1 -0
- package/dist/esm/FileBrowser/AttachLibrary.mjs +2 -2
- package/dist/esm/FileBrowser/AttachLibrary.mjs.map +1 -1
- package/dist/esm/FileBrowser/AttachLibrary.native.js +8 -2
- package/dist/esm/FileBrowser/AttachLibrary.native.js.map +1 -1
- package/dist/esm/FrappeChartCard.mjs +1 -1
- package/dist/esm/FrappeChartCard.native.js +1 -1
- package/dist/esm/FrappeChartCard.native.js.map +1 -1
- package/dist/esm/FrappeDashboard.mjs +6 -27
- package/dist/esm/FrappeDashboard.mjs.map +1 -1
- package/dist/esm/FrappeDashboard.native.js +6 -33
- package/dist/esm/FrappeDashboard.native.js.map +1 -1
- package/dist/esm/FrappeImageView.mjs +2 -7
- package/dist/esm/FrappeImageView.mjs.map +1 -1
- package/dist/esm/FrappeImageView.native.js +2 -8
- package/dist/esm/FrappeImageView.native.js.map +1 -1
- package/dist/esm/fields/CodeEditor/index.mjs +4 -0
- package/dist/esm/fields/CodeEditor/index.mjs.map +1 -1
- package/dist/esm/fields/CodeEditor/index.native.js +4 -0
- package/dist/esm/fields/CodeEditor/index.native.js.map +1 -1
- package/dist/esm/fields/LinkTree.mjs +5 -3
- package/dist/esm/fields/LinkTree.mjs.map +1 -1
- package/dist/esm/fields/LinkTree.native.js +9 -5
- package/dist/esm/fields/LinkTree.native.js.map +1 -1
- package/dist/esm/formEngine/createPrefill.mjs +11 -0
- package/dist/esm/formEngine/createPrefill.mjs.map +1 -0
- package/dist/esm/formEngine/createPrefill.native.js +11 -0
- package/dist/esm/formEngine/createPrefill.native.js.map +1 -0
- package/dist/esm/formEngine/dirtyTracking.mjs +28 -0
- package/dist/esm/formEngine/dirtyTracking.mjs.map +1 -0
- package/dist/esm/formEngine/dirtyTracking.native.js +47 -0
- package/dist/esm/formEngine/dirtyTracking.native.js.map +1 -0
- package/dist/esm/formEngine/doctypeMeta.mjs +2 -0
- package/dist/esm/formEngine/doctypeMeta.mjs.map +1 -0
- package/dist/esm/formEngine/doctypeMeta.native.js +2 -0
- package/dist/esm/formEngine/doctypeMeta.native.js.map +1 -0
- package/dist/esm/formEngine/fetchFrom.mjs +40 -0
- package/dist/esm/formEngine/fetchFrom.mjs.map +1 -0
- package/dist/esm/formEngine/fetchFrom.native.js +80 -0
- package/dist/esm/formEngine/fetchFrom.native.js.map +1 -0
- package/dist/esm/formEngine/fieldPermissions.mjs +28 -0
- package/dist/esm/formEngine/fieldPermissions.mjs.map +1 -0
- package/dist/esm/formEngine/fieldPermissions.native.js +50 -0
- package/dist/esm/formEngine/fieldPermissions.native.js.map +1 -0
- package/dist/esm/formEngine/formActions.mjs +146 -0
- package/dist/esm/formEngine/formActions.mjs.map +1 -0
- package/dist/esm/formEngine/formActions.native.js +153 -0
- package/dist/esm/formEngine/formActions.native.js.map +1 -0
- package/dist/esm/formEngine/formLayout.mjs +217 -0
- package/dist/esm/formEngine/formLayout.mjs.map +1 -0
- package/dist/esm/formEngine/formLayout.native.js +357 -0
- package/dist/esm/formEngine/formLayout.native.js.map +1 -0
- package/dist/esm/formEngine/index.mjs +8 -0
- package/dist/esm/formEngine/index.mjs.map +1 -0
- package/dist/esm/formEngine/index.native.js +8 -0
- package/dist/esm/formEngine/index.native.js.map +1 -0
- package/dist/esm/formEngine/useFormEngine.mjs +362 -0
- package/dist/esm/formEngine/useFormEngine.mjs.map +1 -0
- package/dist/esm/formEngine/useFormEngine.native.js +448 -0
- package/dist/esm/formEngine/useFormEngine.native.js.map +1 -0
- package/dist/esm/frappeStoryTarget.mjs +4 -1
- package/dist/esm/frappeStoryTarget.mjs.map +1 -1
- package/dist/esm/frappeStoryTarget.native.js +4 -2
- package/dist/esm/frappeStoryTarget.native.js.map +1 -1
- package/dist/esm/index.mjs +4 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +4 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/jsx/ChartCard.mjs +27 -0
- package/dist/jsx/ChartCard.mjs.map +1 -0
- package/dist/jsx/ChartCard.native.js +72 -0
- package/dist/jsx/ChartCard.native.js.map +1 -0
- package/dist/jsx/FileBrowser/AttachLibrary.mjs +2 -2
- package/dist/jsx/FileBrowser/AttachLibrary.mjs.map +1 -1
- package/dist/jsx/FileBrowser/AttachLibrary.native.js +8 -2
- package/dist/jsx/FileBrowser/AttachLibrary.native.js.map +1 -1
- package/dist/jsx/FrappeChartCard.mjs +1 -1
- package/dist/jsx/FrappeChartCard.native.js +3 -3
- package/dist/jsx/FrappeChartCard.native.js.map +1 -1
- package/dist/jsx/FrappeDashboard.mjs +6 -27
- package/dist/jsx/FrappeDashboard.mjs.map +1 -1
- package/dist/jsx/FrappeDashboard.native.js +6 -33
- package/dist/jsx/FrappeDashboard.native.js.map +1 -1
- package/dist/jsx/FrappeImageView.mjs +2 -7
- package/dist/jsx/FrappeImageView.mjs.map +1 -1
- package/dist/jsx/FrappeImageView.native.js +2 -9
- package/dist/jsx/FrappeImageView.native.js.map +1 -1
- package/dist/jsx/fields/CodeEditor/index.mjs +4 -0
- package/dist/jsx/fields/CodeEditor/index.mjs.map +1 -1
- package/dist/jsx/fields/CodeEditor/index.native.js +4 -0
- package/dist/jsx/fields/CodeEditor/index.native.js.map +1 -1
- package/dist/jsx/fields/LinkTree.mjs +5 -3
- package/dist/jsx/fields/LinkTree.mjs.map +1 -1
- package/dist/jsx/fields/LinkTree.native.js +9 -5
- package/dist/jsx/fields/LinkTree.native.js.map +1 -1
- package/dist/jsx/formEngine/createPrefill.mjs +11 -0
- package/dist/jsx/formEngine/createPrefill.mjs.map +1 -0
- package/dist/jsx/formEngine/createPrefill.native.js +40 -0
- package/dist/jsx/formEngine/createPrefill.native.js.map +1 -0
- package/dist/jsx/formEngine/dirtyTracking.mjs +28 -0
- package/dist/jsx/formEngine/dirtyTracking.mjs.map +1 -0
- package/dist/jsx/formEngine/dirtyTracking.native.js +77 -0
- package/dist/jsx/formEngine/dirtyTracking.native.js.map +1 -0
- package/dist/jsx/formEngine/doctypeMeta.mjs +2 -0
- package/dist/jsx/formEngine/doctypeMeta.mjs.map +1 -0
- package/dist/jsx/formEngine/doctypeMeta.native.js +21 -0
- package/dist/jsx/formEngine/doctypeMeta.native.js.map +1 -0
- package/dist/jsx/formEngine/fetchFrom.mjs +40 -0
- package/dist/jsx/formEngine/fetchFrom.mjs.map +1 -0
- package/dist/jsx/formEngine/fetchFrom.native.js +110 -0
- package/dist/jsx/formEngine/fetchFrom.native.js.map +1 -0
- package/dist/jsx/formEngine/fieldPermissions.mjs +28 -0
- package/dist/jsx/formEngine/fieldPermissions.mjs.map +1 -0
- package/dist/jsx/formEngine/fieldPermissions.native.js +80 -0
- package/dist/jsx/formEngine/fieldPermissions.native.js.map +1 -0
- package/dist/jsx/formEngine/formActions.mjs +146 -0
- package/dist/jsx/formEngine/formActions.mjs.map +1 -0
- package/dist/jsx/formEngine/formActions.native.js +199 -0
- package/dist/jsx/formEngine/formActions.native.js.map +1 -0
- package/dist/jsx/formEngine/formLayout.mjs +217 -0
- package/dist/jsx/formEngine/formLayout.mjs.map +1 -0
- package/dist/jsx/formEngine/formLayout.native.js +388 -0
- package/dist/jsx/formEngine/formLayout.native.js.map +1 -0
- package/dist/jsx/formEngine/index.mjs +8 -0
- package/dist/jsx/formEngine/index.mjs.map +1 -0
- package/dist/jsx/formEngine/index.native.js +51 -0
- package/dist/jsx/formEngine/index.native.js.map +1 -0
- package/dist/jsx/formEngine/useFormEngine.mjs +362 -0
- package/dist/jsx/formEngine/useFormEngine.mjs.map +1 -0
- package/dist/jsx/formEngine/useFormEngine.native.js +476 -0
- package/dist/jsx/formEngine/useFormEngine.native.js.map +1 -0
- package/dist/jsx/frappeStoryTarget.mjs +4 -1
- package/dist/jsx/frappeStoryTarget.mjs.map +1 -1
- package/dist/jsx/frappeStoryTarget.native.js +4 -3
- package/dist/jsx/frappeStoryTarget.native.js.map +1 -1
- package/dist/jsx/index.js +4 -2
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +4 -2
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +19 -1
- package/dist/jsx/index.native.js.map +1 -1
- package/package.json +23 -17
- package/src/ChartCard.tsx +31 -0
- package/src/FileBrowser/AttachLibrary.tsx +13 -1
- package/src/FileBrowser/FileBrowser.spec.tsx +16 -0
- package/src/FileBrowser/FileBrowser.stories.tsx +18 -1
- package/src/FileBrowser/folderTree.spec.tsx +68 -0
- package/src/FrappeCalendar.stories.tsx +1 -2
- package/src/FrappeChartCard.stories.tsx +1 -1
- package/src/FrappeChartCard.tsx +1 -1
- package/src/FrappeDashboard.stories.tsx +6 -5
- package/src/FrappeDashboard.tsx +6 -33
- package/src/FrappeGantt.stories.tsx +1 -2
- package/src/FrappeImageView.tsx +16 -15
- package/src/FrappeList.stories.tsx +3 -10
- package/src/classicScriptSafety.spec.ts +47 -0
- package/src/components/attachments.stories.tsx +9 -2
- package/src/dependsOn.ts +2 -2
- package/src/desk/DeskFormSidebar.stories.tsx +19 -1
- package/src/fields/CodeEditor/CodeEditor.spec.tsx +37 -0
- package/src/fields/CodeEditor/CodeEditor.stories.tsx +83 -66
- package/src/fields/CodeEditor/index.tsx +4 -0
- package/src/fields/DateRange/DateRange.spec.tsx +143 -0
- package/src/fields/FileUpload.spec.tsx +334 -0
- package/src/fields/FileUploadField/FileUploadField.spec.tsx +166 -0
- package/src/fields/HTML/Html.spec.tsx +179 -0
- package/src/fields/Icon/Icon.stories.tsx +138 -0
- package/src/fields/Link.stories.tsx +2 -0
- package/src/fields/LinkTree.tsx +18 -3
- package/src/fields/LongText/LongText.spec.tsx +217 -0
- package/src/fields/OTPInput/OTPInput.spec.tsx +167 -0
- package/src/fields/Password/Password.stories.tsx +21 -0
- package/src/fields/Phone/Phone.spec.tsx +151 -0
- package/src/fields/Progress/Progress.spec.tsx +160 -0
- package/src/fields/ReadOnly/ReadOnly.spec.tsx +229 -0
- package/src/fields/SmallText/SmallText.spec.tsx +175 -0
- package/src/fields/shared/EditorPreviewPanes.spec.tsx +122 -0
- package/src/formEngine/createPrefill.ts +28 -0
- package/src/formEngine/dirtyTracking.spec.ts +91 -0
- package/src/formEngine/dirtyTracking.ts +64 -0
- package/src/formEngine/doctypeMeta.ts +117 -0
- package/src/formEngine/fetchFrom.spec.ts +92 -0
- package/src/formEngine/fetchFrom.ts +111 -0
- package/src/formEngine/fieldPermissions.spec.ts +61 -0
- package/src/formEngine/fieldPermissions.ts +68 -0
- package/src/formEngine/formActions.ts +285 -0
- package/src/formEngine/formLayout.spec.ts +373 -0
- package/src/formEngine/formLayout.ts +392 -0
- package/src/formEngine/index.ts +33 -0
- package/src/formEngine/seam.spec.ts +148 -0
- package/src/formEngine/useFormEngine.ts +496 -0
- package/src/frappeRemainder.designLaw.spec.ts +9 -0
- package/src/frappeStoryTarget.tsx +25 -9
- package/src/frappeViews.designLaw.spec.ts +1 -0
- package/src/index.spec.ts +7 -3
- package/src/index.ts +51 -1
- package/src/schemaForm/SchemaForm.spec.tsx +4 -1
- package/types/ChartCard.d.ts +13 -0
- package/types/ChartCard.d.ts.map +1 -0
- package/types/FileBrowser/AttachLibrary.d.ts.map +1 -1
- package/types/FrappeDashboard.d.ts +2 -10
- package/types/FrappeDashboard.d.ts.map +1 -1
- package/types/FrappeImageView.d.ts.map +1 -1
- package/types/dependsOn.d.ts +2 -2
- package/types/fields/CodeEditor/index.d.ts.map +1 -1
- package/types/fields/LinkTree.d.ts.map +1 -1
- package/types/formEngine/createPrefill.d.ts +19 -0
- package/types/formEngine/createPrefill.d.ts.map +1 -0
- package/types/formEngine/dirtyTracking.d.ts +36 -0
- package/types/formEngine/dirtyTracking.d.ts.map +1 -0
- package/types/formEngine/doctypeMeta.d.ts +117 -0
- package/types/formEngine/doctypeMeta.d.ts.map +1 -0
- package/types/formEngine/fetchFrom.d.ts +43 -0
- package/types/formEngine/fetchFrom.d.ts.map +1 -0
- package/types/formEngine/fieldPermissions.d.ts +38 -0
- package/types/formEngine/fieldPermissions.d.ts.map +1 -0
- package/types/formEngine/formActions.d.ts +95 -0
- package/types/formEngine/formActions.d.ts.map +1 -0
- package/types/formEngine/formLayout.d.ts +91 -0
- package/types/formEngine/formLayout.d.ts.map +1 -0
- package/types/formEngine/index.d.ts +27 -0
- package/types/formEngine/index.d.ts.map +1 -0
- package/types/formEngine/useFormEngine.d.ts +61 -0
- package/types/formEngine/useFormEngine.d.ts.map +1 -0
- package/types/frappeStoryTarget.d.ts +2 -2
- package/types/frappeStoryTarget.d.ts.map +1 -1
- package/types/index.d.ts +4 -1
- package/types/index.d.ts.map +1 -1
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The desk form state engine.
|
|
3
|
+
*
|
|
4
|
+
* - Edit mode: the doc lives in a name-filtered live collection
|
|
5
|
+
* (useFrappeCollection + useLiveQuery) so realtime updates flow in; saves
|
|
6
|
+
* write only dirty fields through the collection's optimistic update path
|
|
7
|
+
* (live update_doc enforces If-Match OCC — a version conflict surfaces as
|
|
8
|
+
* a save error and flips `conflict` so the UI can offer a reload).
|
|
9
|
+
* - Create mode: seeds meta defaults (+ optional Duplicate/Amend prefill)
|
|
10
|
+
* and inserts via direct REST so the server-issued name is returned for
|
|
11
|
+
* router.replace(formPath(...)).
|
|
12
|
+
* - Dirty tracking against the last server baseline; external updates land
|
|
13
|
+
* on clean fields in place, dirty fields are preserved.
|
|
14
|
+
* - Submit / Cancel lifecycle for is_submittable doctypes via
|
|
15
|
+
* frappe.client.submit / frappe.client.cancel (probed) — realtime then
|
|
16
|
+
* refreshes the live doc; Amend is a create-mode copy with amended_from.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { eq, useFrappeCollection, useLiveQuery } from "@multiplatform.one/frappe";
|
|
20
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
21
|
+
import type { DoctypeMeta } from "./doctypeMeta";
|
|
22
|
+
import type { FrappeConnection } from "./formActions";
|
|
23
|
+
import {
|
|
24
|
+
cancelDoc,
|
|
25
|
+
createHttpClient,
|
|
26
|
+
getLoggedUser,
|
|
27
|
+
insertDoc,
|
|
28
|
+
submitDoc,
|
|
29
|
+
validateLink,
|
|
30
|
+
} from "./formActions";
|
|
31
|
+
import { buildCreateDefaults, isPromptNamed } from "./formLayout";
|
|
32
|
+
import { takeCreatePrefill } from "./createPrefill";
|
|
33
|
+
import { evaluateDependsOn } from "../dependsOn";
|
|
34
|
+
import { computeDirtyFields, isEmptyValue, isSameValue } from "./dirtyTracking";
|
|
35
|
+
import { applyFetchedValues, buildFetchMap } from "./fetchFrom";
|
|
36
|
+
|
|
37
|
+
type DocRecord = Record<string, unknown> & { name: string; doctype: string };
|
|
38
|
+
|
|
39
|
+
export interface FormEngine {
|
|
40
|
+
/** Current (locally edited) doc values. */
|
|
41
|
+
doc: Record<string, unknown>;
|
|
42
|
+
/** Last authoritative server snapshot (undefined in create mode). */
|
|
43
|
+
baseline: Record<string, unknown> | undefined;
|
|
44
|
+
isLoading: boolean;
|
|
45
|
+
/** Edit-mode doc missing after load settles (bad name / no permission). */
|
|
46
|
+
notFound: boolean;
|
|
47
|
+
/** The open doc was deleted on the server while open. */
|
|
48
|
+
wasDeleted: boolean;
|
|
49
|
+
isCreate: boolean;
|
|
50
|
+
isDirty: boolean;
|
|
51
|
+
dirtyFields: string[];
|
|
52
|
+
isSaving: boolean;
|
|
53
|
+
/** Per-field + `general` error messages. */
|
|
54
|
+
errors: Record<string, string>;
|
|
55
|
+
/** True after a save failed with a version conflict (OCC 409). */
|
|
56
|
+
conflict: boolean;
|
|
57
|
+
/** Logged-in user (for comments / likes / assignments). */
|
|
58
|
+
currentUser: string;
|
|
59
|
+
docstatus: number;
|
|
60
|
+
setValue: (field: string, value: unknown) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Save. Create mode resolves with the server-issued doc name;
|
|
63
|
+
* edit mode resolves with the current name. Undefined = validation failed.
|
|
64
|
+
*/
|
|
65
|
+
save: () => Promise<string | undefined>;
|
|
66
|
+
/** Submit lifecycle (is_submittable): docstatus 0 → 1. */
|
|
67
|
+
submit: () => Promise<boolean>;
|
|
68
|
+
/** Cancel lifecycle: docstatus 1 → 2. */
|
|
69
|
+
cancel: () => Promise<boolean>;
|
|
70
|
+
/** Delete the doc through the live collection. */
|
|
71
|
+
remove: () => Promise<boolean>;
|
|
72
|
+
/** Discard local edits / clear conflict, back to the server baseline. */
|
|
73
|
+
reload: () => void;
|
|
74
|
+
/** Client-side required validation (fires on save too). */
|
|
75
|
+
validate: () => Record<string, string>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const LAYOUT_TYPES = new Set(["Section Break", "Column Break", "Tab Break", "Fold"]);
|
|
79
|
+
|
|
80
|
+
function isConflictError(err: unknown): boolean {
|
|
81
|
+
const e = err as { status?: number; exc_type?: string; message?: string } | undefined;
|
|
82
|
+
if (!e) return false;
|
|
83
|
+
if (e.status === 409) return true;
|
|
84
|
+
if (e.exc_type === "TimestampMismatchError") return true;
|
|
85
|
+
return /TimestampMismatch|has been modified/i.test(e.message ?? "");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractServerError(err: unknown, fallback: string): string {
|
|
89
|
+
const e = err as { _server_messages?: string; message?: string; exc_type?: string } | undefined;
|
|
90
|
+
if (e?._server_messages) {
|
|
91
|
+
try {
|
|
92
|
+
const messages = JSON.parse(e._server_messages) as string[];
|
|
93
|
+
const parsed = messages
|
|
94
|
+
.map((msg) => {
|
|
95
|
+
try {
|
|
96
|
+
return (JSON.parse(msg) as { message?: string }).message ?? "";
|
|
97
|
+
} catch {
|
|
98
|
+
return msg;
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
if (parsed.length > 0) return parsed.join("\n");
|
|
103
|
+
} catch {
|
|
104
|
+
// fall through
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return e?.message || fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function useFormEngine(
|
|
111
|
+
connection: FrappeConnection | undefined,
|
|
112
|
+
doctype: string,
|
|
113
|
+
/** Document name, or undefined for create mode. */
|
|
114
|
+
name: string | undefined,
|
|
115
|
+
meta: DoctypeMeta | undefined,
|
|
116
|
+
translate: (key: string, options?: Record<string, unknown>) => string,
|
|
117
|
+
): FormEngine {
|
|
118
|
+
const isCreate = !name;
|
|
119
|
+
const [localDoc, setLocalDoc] = useState<Record<string, unknown>>({});
|
|
120
|
+
const [baseline, setBaseline] = useState<Record<string, unknown> | undefined>(undefined);
|
|
121
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
122
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
123
|
+
const [conflict, setConflict] = useState(false);
|
|
124
|
+
const [wasDeleted, setWasDeleted] = useState(false);
|
|
125
|
+
const [currentUser, setCurrentUser] = useState("");
|
|
126
|
+
const everLoadedRef = useRef(false);
|
|
127
|
+
const seededRef = useRef(false);
|
|
128
|
+
const localDocRef = useRef(localDoc);
|
|
129
|
+
localDocRef.current = localDoc;
|
|
130
|
+
const baselineRef = useRef(baseline);
|
|
131
|
+
baselineRef.current = baseline;
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------
|
|
134
|
+
// Live doc subscription (edit mode)
|
|
135
|
+
// ---------------------------------------------------------------------
|
|
136
|
+
const docCollection = useFrappeCollection<DocRecord>(name ? connection : undefined, doctype, {
|
|
137
|
+
filters: name ? { name } : undefined,
|
|
138
|
+
});
|
|
139
|
+
const { data: rawDocData, isLoading: isDocLoading } = useLiveQuery(
|
|
140
|
+
(q) => {
|
|
141
|
+
if (!docCollection || !name) return null;
|
|
142
|
+
return q.from({ _doc: docCollection }).where(({ _doc }) => eq(_doc.name, name));
|
|
143
|
+
},
|
|
144
|
+
[docCollection, name],
|
|
145
|
+
);
|
|
146
|
+
const liveDoc = useMemo(() => {
|
|
147
|
+
const arr = Array.isArray(rawDocData) ? rawDocData : rawDocData ? [rawDocData] : [];
|
|
148
|
+
return (arr[0] ?? null) as DocRecord | null;
|
|
149
|
+
}, [rawDocData]);
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------
|
|
152
|
+
// Session user
|
|
153
|
+
// ---------------------------------------------------------------------
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!connection) return;
|
|
156
|
+
let cancelled = false;
|
|
157
|
+
getLoggedUser(createHttpClient(connection))
|
|
158
|
+
.then((user) => {
|
|
159
|
+
if (!cancelled) setCurrentUser(user);
|
|
160
|
+
})
|
|
161
|
+
.catch(() => {
|
|
162
|
+
// Anonymous fallback — comment composer degrades gracefully.
|
|
163
|
+
});
|
|
164
|
+
return () => {
|
|
165
|
+
cancelled = true;
|
|
166
|
+
};
|
|
167
|
+
}, [connection?.baseURL]);
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------
|
|
170
|
+
// Reset per-document
|
|
171
|
+
// ---------------------------------------------------------------------
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
everLoadedRef.current = false;
|
|
174
|
+
seededRef.current = false;
|
|
175
|
+
setWasDeleted(false);
|
|
176
|
+
setConflict(false);
|
|
177
|
+
setErrors({});
|
|
178
|
+
setBaseline(undefined);
|
|
179
|
+
setLocalDoc({});
|
|
180
|
+
}, [doctype, name]);
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------
|
|
183
|
+
// Create-mode seeding (meta defaults + Duplicate/Amend prefill)
|
|
184
|
+
// ---------------------------------------------------------------------
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!isCreate || !meta || seededRef.current) return;
|
|
187
|
+
seededRef.current = true;
|
|
188
|
+
const defaults = buildCreateDefaults(meta.fields, currentUser || undefined);
|
|
189
|
+
const prefill = takeCreatePrefill(doctype);
|
|
190
|
+
setLocalDoc({ ...defaults, ...prefill, doctype });
|
|
191
|
+
// currentUser intentionally not a dep: seeding happens once per mount;
|
|
192
|
+
// a late-resolving user only matters for brand-new blank forms where
|
|
193
|
+
// the meta usually arrives later anyway.
|
|
194
|
+
}, [isCreate, meta, doctype]);
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------
|
|
197
|
+
// Field-level in-place sync (edit mode): server updates land on clean
|
|
198
|
+
// fields; dirty fields keep the user's edits.
|
|
199
|
+
// ---------------------------------------------------------------------
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (!name) return;
|
|
202
|
+
if (liveDoc) {
|
|
203
|
+
everLoadedRef.current = true;
|
|
204
|
+
setWasDeleted(false);
|
|
205
|
+
const incoming = liveDoc as Record<string, unknown>;
|
|
206
|
+
const prevBaseline = baselineRef.current;
|
|
207
|
+
if (!prevBaseline) {
|
|
208
|
+
setBaseline(incoming);
|
|
209
|
+
setLocalDoc(incoming);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const local = localDocRef.current;
|
|
213
|
+
const merged: Record<string, unknown> = { ...incoming };
|
|
214
|
+
for (const key of Object.keys(local)) {
|
|
215
|
+
// Same predicate as dirty tracking: a normalization-identical local
|
|
216
|
+
// value is not an edit, so the incoming server value wins.
|
|
217
|
+
if (!isSameValue(local[key], prevBaseline[key])) merged[key] = local[key];
|
|
218
|
+
}
|
|
219
|
+
setBaseline(incoming);
|
|
220
|
+
setLocalDoc(merged);
|
|
221
|
+
} else if (everLoadedRef.current && !isDocLoading) {
|
|
222
|
+
setWasDeleted(true);
|
|
223
|
+
}
|
|
224
|
+
}, [liveDoc, isDocLoading, name]);
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------
|
|
227
|
+
// Dirty tracking
|
|
228
|
+
// ---------------------------------------------------------------------
|
|
229
|
+
const dirtyFields = useMemo(() => computeDirtyFields(localDoc, baseline), [localDoc, baseline]);
|
|
230
|
+
const isDirty = dirtyFields.length > 0;
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------
|
|
233
|
+
// fetch_from preview: when a Link with dependent fetch_from targets
|
|
234
|
+
// changes, pull the source columns via frappe.client.validate_link and
|
|
235
|
+
// patch the targets (desk validate_link_and_fetch). Server stays
|
|
236
|
+
// authoritative on save.
|
|
237
|
+
// ---------------------------------------------------------------------
|
|
238
|
+
const fetchMap = useMemo(() => (meta ? buildFetchMap(meta.fields) : undefined), [meta]);
|
|
239
|
+
const fetchStateRef = useRef<{
|
|
240
|
+
meta: DoctypeMeta | undefined;
|
|
241
|
+
connection: FrappeConnection | undefined;
|
|
242
|
+
}>({ meta: undefined, connection: undefined });
|
|
243
|
+
fetchStateRef.current = { meta, connection };
|
|
244
|
+
const fetchMapRef = useRef(fetchMap);
|
|
245
|
+
fetchMapRef.current = fetchMap;
|
|
246
|
+
|
|
247
|
+
const runFetchFrom = useCallback((field: string, value: unknown) => {
|
|
248
|
+
const rules = fetchMapRef.current?.get(field);
|
|
249
|
+
if (!rules || rules.length === 0) return;
|
|
250
|
+
const { meta: currentMeta, connection: currentConnection } = fetchStateRef.current;
|
|
251
|
+
const linkField = currentMeta?.fields.find((f) => f.fieldname === field);
|
|
252
|
+
const targetDoctype = typeof linkField?.options === "string" ? linkField.options : "";
|
|
253
|
+
const applyPatch = (fetched: Record<string, unknown>) => {
|
|
254
|
+
setLocalDoc((prev) => {
|
|
255
|
+
// The link changed again while the fetch was in flight — drop it.
|
|
256
|
+
if (prev[field] !== value) return prev;
|
|
257
|
+
const patch = applyFetchedValues(rules, fetched, prev);
|
|
258
|
+
return Object.keys(patch).length > 0 ? { ...prev, ...patch } : prev;
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
if (value === undefined || value === null || value === "") {
|
|
262
|
+
// Cleared link → desk blanks the dependent targets.
|
|
263
|
+
applyPatch({});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!currentConnection || !targetDoctype || currentConnection.fixtures) return;
|
|
267
|
+
const sources = Array.from(new Set(rules.map((rule) => rule.source)));
|
|
268
|
+
validateLink(createHttpClient(currentConnection), targetDoctype, String(value), sources)
|
|
269
|
+
.then(applyPatch)
|
|
270
|
+
.catch(() => {
|
|
271
|
+
// Invalid link / no read perm on source — leave targets as-is;
|
|
272
|
+
// the server re-validates on save.
|
|
273
|
+
});
|
|
274
|
+
}, []);
|
|
275
|
+
|
|
276
|
+
const setValue = useCallback(
|
|
277
|
+
(field: string, value: unknown) => {
|
|
278
|
+
setLocalDoc((prev) => ({ ...prev, [field]: value }));
|
|
279
|
+
setErrors((prev) => {
|
|
280
|
+
if (!(field in prev)) return prev;
|
|
281
|
+
const next = { ...prev };
|
|
282
|
+
delete next[field];
|
|
283
|
+
return next;
|
|
284
|
+
});
|
|
285
|
+
runFetchFrom(field, value);
|
|
286
|
+
},
|
|
287
|
+
[runFetchFrom],
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------
|
|
291
|
+
// Validation
|
|
292
|
+
// ---------------------------------------------------------------------
|
|
293
|
+
const validate = useCallback((): Record<string, string> => {
|
|
294
|
+
if (!meta) return {};
|
|
295
|
+
// Same evaluation context desk gives depends_on: `__islocal` marks an
|
|
296
|
+
// unsaved doc, `__session_user` backs `frappe.session.user` references.
|
|
297
|
+
const doc: Record<string, unknown> = {
|
|
298
|
+
...localDocRef.current,
|
|
299
|
+
__islocal: isCreate ? 1 : 0,
|
|
300
|
+
...(currentUser ? { __session_user: currentUser } : {}),
|
|
301
|
+
};
|
|
302
|
+
const validationErrors: Record<string, string> = {};
|
|
303
|
+
// Prompt-named doctypes need a user-entered name before insert
|
|
304
|
+
// (probed: REST insert without one fails with "Please set the document name").
|
|
305
|
+
if (isCreate && isPromptNamed(meta.autoname, meta.naming_rule) && isEmptyValue(doc.name)) {
|
|
306
|
+
validationErrors.name = translate("nameRequired", {
|
|
307
|
+
defaultValue: "Name is required",
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
for (const field of meta.fields) {
|
|
311
|
+
if (field.hidden || LAYOUT_TYPES.has(field.fieldtype)) continue;
|
|
312
|
+
// Desk parity: a field hidden by depends_on is not enforced client-side.
|
|
313
|
+
if (!evaluateDependsOn(field.depends_on, doc, true)) continue;
|
|
314
|
+
const required =
|
|
315
|
+
!!field.reqd ||
|
|
316
|
+
(!!field.mandatory_depends_on && evaluateDependsOn(field.mandatory_depends_on, doc, false));
|
|
317
|
+
if (!required) continue;
|
|
318
|
+
if (isEmptyValue(doc[field.fieldname])) {
|
|
319
|
+
validationErrors[field.fieldname] = translate("fieldRequired", {
|
|
320
|
+
defaultValue: "{{label}} is required",
|
|
321
|
+
label: field.label || field.fieldname,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return validationErrors;
|
|
326
|
+
}, [meta, isCreate, currentUser, translate]);
|
|
327
|
+
|
|
328
|
+
// ---------------------------------------------------------------------
|
|
329
|
+
// Save
|
|
330
|
+
// ---------------------------------------------------------------------
|
|
331
|
+
const save = useCallback(async (): Promise<string | undefined> => {
|
|
332
|
+
if (wasDeleted || !connection) return undefined;
|
|
333
|
+
const requiredErrors = validate();
|
|
334
|
+
if (Object.keys(requiredErrors).length > 0) {
|
|
335
|
+
setErrors(requiredErrors);
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
setIsSaving(true);
|
|
339
|
+
setErrors({});
|
|
340
|
+
try {
|
|
341
|
+
if (name) {
|
|
342
|
+
if (!docCollection) return undefined;
|
|
343
|
+
const local = localDocRef.current;
|
|
344
|
+
const base = baselineRef.current ?? {};
|
|
345
|
+
// Same predicate as dirty tracking: normalization-identical values
|
|
346
|
+
// must not be written back to the server as phantom edits.
|
|
347
|
+
const changed = Object.keys(local).filter(
|
|
348
|
+
(key) => key !== "doctype" && key !== "name" && !isSameValue(local[key], base[key]),
|
|
349
|
+
);
|
|
350
|
+
if (changed.length > 0) {
|
|
351
|
+
const tx = docCollection.update(name, (draft) => {
|
|
352
|
+
for (const key of changed) {
|
|
353
|
+
(draft as Record<string, unknown>)[key] = local[key];
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
await tx.isPersisted.promise;
|
|
357
|
+
}
|
|
358
|
+
setConflict(false);
|
|
359
|
+
return name;
|
|
360
|
+
}
|
|
361
|
+
// Create mode: direct REST insert to obtain the server-issued name.
|
|
362
|
+
// A user-entered `name` is kept for prompt-named doctypes; empty names
|
|
363
|
+
// are stripped so hash/series autonaming applies.
|
|
364
|
+
const { doctype: _dt, name: newName, ...values } = localDocRef.current;
|
|
365
|
+
if (typeof newName === "string" && newName.trim()) {
|
|
366
|
+
values.name = newName.trim();
|
|
367
|
+
}
|
|
368
|
+
const created = await insertDoc(createHttpClient(connection), doctype, values);
|
|
369
|
+
return String(created.name ?? "");
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (isConflictError(err)) {
|
|
372
|
+
setConflict(true);
|
|
373
|
+
setErrors({
|
|
374
|
+
general: translate("conflictError", {
|
|
375
|
+
defaultValue:
|
|
376
|
+
"This document was modified on the server after you opened it. Reload to get the latest version — your unsaved edits will be discarded.",
|
|
377
|
+
}),
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
setErrors({
|
|
381
|
+
general: extractServerError(
|
|
382
|
+
err,
|
|
383
|
+
translate("saveFailed", { defaultValue: "Failed to save" }),
|
|
384
|
+
),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
} finally {
|
|
389
|
+
setIsSaving(false);
|
|
390
|
+
}
|
|
391
|
+
}, [wasDeleted, connection, validate, name, docCollection, doctype, translate]);
|
|
392
|
+
|
|
393
|
+
// ---------------------------------------------------------------------
|
|
394
|
+
// Submit / Cancel lifecycle
|
|
395
|
+
// ---------------------------------------------------------------------
|
|
396
|
+
const submit = useCallback(async (): Promise<boolean> => {
|
|
397
|
+
if (!connection || !name) return false;
|
|
398
|
+
setIsSaving(true);
|
|
399
|
+
setErrors({});
|
|
400
|
+
try {
|
|
401
|
+
// The full current server doc (incl. `modified`) is required for OCC.
|
|
402
|
+
const base = baselineRef.current;
|
|
403
|
+
if (!base) return false;
|
|
404
|
+
await submitDoc(createHttpClient(connection), { ...base, doctype });
|
|
405
|
+
return true;
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (isConflictError(err)) setConflict(true);
|
|
408
|
+
setErrors({
|
|
409
|
+
general: extractServerError(
|
|
410
|
+
err,
|
|
411
|
+
translate("submitFailed", { defaultValue: "Failed to submit" }),
|
|
412
|
+
),
|
|
413
|
+
});
|
|
414
|
+
return false;
|
|
415
|
+
} finally {
|
|
416
|
+
setIsSaving(false);
|
|
417
|
+
}
|
|
418
|
+
}, [connection, name, doctype, translate]);
|
|
419
|
+
|
|
420
|
+
const cancel = useCallback(async (): Promise<boolean> => {
|
|
421
|
+
if (!connection || !name) return false;
|
|
422
|
+
setIsSaving(true);
|
|
423
|
+
setErrors({});
|
|
424
|
+
try {
|
|
425
|
+
await cancelDoc(createHttpClient(connection), doctype, name);
|
|
426
|
+
return true;
|
|
427
|
+
} catch (err) {
|
|
428
|
+
setErrors({
|
|
429
|
+
general: extractServerError(
|
|
430
|
+
err,
|
|
431
|
+
translate("cancelFailed", { defaultValue: "Failed to cancel" }),
|
|
432
|
+
),
|
|
433
|
+
});
|
|
434
|
+
return false;
|
|
435
|
+
} finally {
|
|
436
|
+
setIsSaving(false);
|
|
437
|
+
}
|
|
438
|
+
}, [connection, name, doctype, translate]);
|
|
439
|
+
|
|
440
|
+
const remove = useCallback(async (): Promise<boolean> => {
|
|
441
|
+
if (!docCollection || !name || wasDeleted) return false;
|
|
442
|
+
setIsSaving(true);
|
|
443
|
+
setErrors({});
|
|
444
|
+
try {
|
|
445
|
+
const tx = docCollection.delete(name);
|
|
446
|
+
await tx.isPersisted.promise;
|
|
447
|
+
return true;
|
|
448
|
+
} catch (err) {
|
|
449
|
+
setErrors({
|
|
450
|
+
general: extractServerError(
|
|
451
|
+
err,
|
|
452
|
+
translate("deleteFailed", { defaultValue: "Failed to delete" }),
|
|
453
|
+
),
|
|
454
|
+
});
|
|
455
|
+
return false;
|
|
456
|
+
} finally {
|
|
457
|
+
setIsSaving(false);
|
|
458
|
+
}
|
|
459
|
+
}, [docCollection, name, wasDeleted, translate]);
|
|
460
|
+
|
|
461
|
+
const reload = useCallback(() => {
|
|
462
|
+
setConflict(false);
|
|
463
|
+
setErrors({});
|
|
464
|
+
if (baselineRef.current) {
|
|
465
|
+
setLocalDoc(baselineRef.current);
|
|
466
|
+
} else if (!name) {
|
|
467
|
+
seededRef.current = false;
|
|
468
|
+
setLocalDoc({ doctype });
|
|
469
|
+
}
|
|
470
|
+
}, [doctype, name]);
|
|
471
|
+
|
|
472
|
+
const docstatus = Number((baseline as { docstatus?: unknown } | undefined)?.docstatus ?? 0);
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
doc: localDoc,
|
|
476
|
+
baseline,
|
|
477
|
+
isLoading: !!name && isDocLoading && !everLoadedRef.current,
|
|
478
|
+
notFound: !!name && !isDocLoading && !liveDoc && !everLoadedRef.current,
|
|
479
|
+
wasDeleted,
|
|
480
|
+
isCreate,
|
|
481
|
+
isDirty,
|
|
482
|
+
dirtyFields,
|
|
483
|
+
isSaving,
|
|
484
|
+
errors,
|
|
485
|
+
conflict,
|
|
486
|
+
currentUser,
|
|
487
|
+
docstatus,
|
|
488
|
+
setValue,
|
|
489
|
+
save,
|
|
490
|
+
submit,
|
|
491
|
+
cancel,
|
|
492
|
+
remove,
|
|
493
|
+
reload,
|
|
494
|
+
validate,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
@@ -75,6 +75,15 @@ describe("Frappe remainder wrappers — design-law 100%", () => {
|
|
|
75
75
|
expect(source).not.toContain("<Tint");
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
it("FrappeChartCard does not import the t5 FrappeDashboard module", () => {
|
|
79
|
+
const card = read("FrappeChartCard.tsx");
|
|
80
|
+
const stories = read("FrappeChartCard.stories.tsx");
|
|
81
|
+
expect(card).not.toMatch(/from ["']\.\/FrappeDashboard["']/);
|
|
82
|
+
expect(stories).not.toMatch(/from ["']\.\/FrappeDashboard["']/);
|
|
83
|
+
expect(card).toMatch(/from ["']\.\/ChartCard["']/);
|
|
84
|
+
expect(stories).toMatch(/from ["']\.\/ChartCard["']/);
|
|
85
|
+
});
|
|
86
|
+
|
|
78
87
|
it("FrappeImageView uses catalog Card, controlIcon icons, and no media pip (R11/R12)", () => {
|
|
79
88
|
const source = read("FrappeImageView.tsx");
|
|
80
89
|
expect(source).toContain("Card");
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The Storybook dev server (apps/storybook/.storybook/main.ts) probes the
|
|
5
5
|
* bench selected by `STORYBOOK_FRAPPE_URL` (default: the local dev bench at
|
|
6
|
-
* http://localhost:8000) and
|
|
7
|
-
* `
|
|
6
|
+
* http://localhost:8000) and defines the result as
|
|
7
|
+
* `process.env.STORYBOOK_FRAPPE_LIVE`. When the bench is reachable it
|
|
8
8
|
* also proxies `/api` and `/socket.io` same-origin (the bench sends no CORS
|
|
9
9
|
* headers) and injects the API token server-side, so no credentials ever
|
|
10
10
|
* reach the preview bundle.
|
|
@@ -22,15 +22,31 @@ interface StorybookFrappeEnv {
|
|
|
22
22
|
STORYBOOK_FRAPPE_URL?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The two keys, read as STATIC `process.env.<KEY>` member expressions.
|
|
27
|
+
*
|
|
28
|
+
* This used to read `import.meta.env`. MPO-192 ruled that out: two bundles
|
|
29
|
+
* execute mpo as a CLASSIC script — the vxrn/rolldown native bundle for
|
|
30
|
+
* Hermes, and any Metro bundle for platform=web, which is what an Expo DOM
|
|
31
|
+
* component (`'use dom'`) is — and in a classic script `import.meta` is a
|
|
32
|
+
* PARSE-time SyntaxError, so the try/catch below never gets to run and the
|
|
33
|
+
* whole module fails to parse.
|
|
34
|
+
*
|
|
35
|
+
* `process.env.<KEY>` is the spelling every bundler on the way to a browser
|
|
36
|
+
* inlines, and it is exactly what public/platform/src/config/runtimeConfig.ts
|
|
37
|
+
* settled on. apps/storybook/.storybook/main.ts defines both keys for the
|
|
38
|
+
* preview bundle (Storybook's own `env` preset only defines the
|
|
39
|
+
* `import.meta.env.*` spelling, so the define is what carries these across).
|
|
40
|
+
* A dynamic `env[key]` read would never be inlined. Where nothing defined the
|
|
41
|
+
* keys and `process` does not exist, the reads throw and the catch answers
|
|
42
|
+
* with the fixtures fallback.
|
|
43
|
+
*/
|
|
31
44
|
const sbEnv: StorybookFrappeEnv = (() => {
|
|
32
45
|
try {
|
|
33
|
-
return
|
|
46
|
+
return {
|
|
47
|
+
STORYBOOK_FRAPPE_LIVE: process.env.STORYBOOK_FRAPPE_LIVE,
|
|
48
|
+
STORYBOOK_FRAPPE_URL: process.env.STORYBOOK_FRAPPE_URL,
|
|
49
|
+
};
|
|
34
50
|
} catch {
|
|
35
51
|
return {};
|
|
36
52
|
}
|
|
@@ -43,6 +43,7 @@ describe("Frappe view adapters — design-law 100%", () => {
|
|
|
43
43
|
expect(source).toContain("formCommonColors");
|
|
44
44
|
expect(source).toContain("knobProps.label");
|
|
45
45
|
expect(source).toContain("knobProps.heading");
|
|
46
|
+
expect(source).toContain("knobProps.textAccentColor");
|
|
46
47
|
expect(source).not.toMatch(/fontSize="\$1"/);
|
|
47
48
|
});
|
|
48
49
|
|
package/src/index.spec.ts
CHANGED
|
@@ -11,14 +11,16 @@
|
|
|
11
11
|
* that seam before anything rendered — measured downstream 2026-08-13.
|
|
12
12
|
* This spec locks the fix both ways:
|
|
13
13
|
*
|
|
14
|
-
* 1. ./FrappeDashboard exports only what it defines (FrappeDashboard
|
|
15
|
-
* the sanitizing
|
|
14
|
+
* 1. ./FrappeDashboard exports only what it defines (FrappeDashboard;
|
|
15
|
+
* ChartCard is a re-export of the sanitizing bridge in ./ChartCard)
|
|
16
|
+
* — no catalog names.
|
|
16
17
|
* 2. The barrel still ships the catalog, and each name IS the components
|
|
17
18
|
* export, not a copy and not a bounce.
|
|
18
19
|
*/
|
|
19
20
|
import * as components from "@multiplatform.one/components";
|
|
20
21
|
import { describe, expect, it } from "vitest";
|
|
21
22
|
import * as dashboard from "./FrappeDashboard";
|
|
23
|
+
import { ChartCard as BridgeChartCard } from "./ChartCard";
|
|
22
24
|
import * as barrel from "./index";
|
|
23
25
|
|
|
24
26
|
const CATALOG_VALUE_NAMES = [
|
|
@@ -37,7 +39,9 @@ describe("dashboard catalog seam", () => {
|
|
|
37
39
|
).toBeUndefined();
|
|
38
40
|
}
|
|
39
41
|
expect(dashboard.FrappeDashboard).toBeDefined();
|
|
40
|
-
expect(dashboard.ChartCard, "the sanitizing ChartCard bridge is
|
|
42
|
+
expect(dashboard.ChartCard, "the sanitizing ChartCard bridge is re-exported").toBe(
|
|
43
|
+
BridgeChartCard,
|
|
44
|
+
);
|
|
41
45
|
expect(
|
|
42
46
|
dashboard.ChartCard,
|
|
43
47
|
"ChartCard must be the sanitizing bridge, not the raw catalog card",
|
package/src/index.ts
CHANGED
|
@@ -81,11 +81,15 @@ export {
|
|
|
81
81
|
|
|
82
82
|
export {
|
|
83
83
|
FrappeDashboard,
|
|
84
|
-
ChartCard,
|
|
85
84
|
type FrappeDashboardProps,
|
|
86
85
|
type DashboardDataSource,
|
|
87
86
|
} from "./FrappeDashboard";
|
|
88
87
|
|
|
88
|
+
// Sanitizing bridge — own file, not FrappeDashboard.tsx (t5). t4
|
|
89
|
+
// FrappeChartCard imports this module; re-exporting from the dashboard
|
|
90
|
+
// file was the catalog import defect.
|
|
91
|
+
export { ChartCard } from "./ChartCard";
|
|
92
|
+
|
|
89
93
|
// The KPI/trend catalog is the components package's — re-export it from
|
|
90
94
|
// there, NOT through ./FrappeDashboard. The old bounce made the built
|
|
91
95
|
// FrappeDashboard.native module re-export names it does not define, and a
|
|
@@ -388,3 +392,49 @@ export {
|
|
|
388
392
|
type UseFrappeReportOptions,
|
|
389
393
|
type UseFrappeReportResult,
|
|
390
394
|
} from "./FrappeReportView";
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// Desk Form Engine (MPO-15)
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
// Meta-driven layout + state engine behind a desk form: tabs/sections,
|
|
401
|
+
// depends_on visibility, mandatory_depends_on, read_only_depends_on,
|
|
402
|
+
// permlevel gating, set_only_once, fetch_from locking, docstatus gating, and
|
|
403
|
+
// the save/submit/cancel/amend lifecycle. Hoisted out of
|
|
404
|
+
// @multiplatform.one/backoffice so FrappeForm can reach it without a
|
|
405
|
+
// higher-tier import; backoffice re-exports these names from their old paths.
|
|
406
|
+
//
|
|
407
|
+
// `formActions` stays deep-import only
|
|
408
|
+
// (`@multiplatform.one/frappe-ui/src/formEngine/formActions`) — its RPC
|
|
409
|
+
// helper names collide with the list and shell surfaces.
|
|
410
|
+
export {
|
|
411
|
+
applyFetchedValues,
|
|
412
|
+
buildCopyValues,
|
|
413
|
+
buildCreateDefaults,
|
|
414
|
+
buildFetchMap,
|
|
415
|
+
buildFormTabs,
|
|
416
|
+
buildPermLevelMatrix,
|
|
417
|
+
canReadLevel,
|
|
418
|
+
canWriteLevel,
|
|
419
|
+
computeDirtyFields,
|
|
420
|
+
isEmptyValue,
|
|
421
|
+
isFetchLocked,
|
|
422
|
+
isPromptNamed,
|
|
423
|
+
isSameValue,
|
|
424
|
+
setCreatePrefill,
|
|
425
|
+
takeCreatePrefill,
|
|
426
|
+
useFormEngine,
|
|
427
|
+
} from "./formEngine";
|
|
428
|
+
export type {
|
|
429
|
+
BuildTabsOptions,
|
|
430
|
+
DocFieldMeta,
|
|
431
|
+
DocLinkMeta,
|
|
432
|
+
DocPermissionRule,
|
|
433
|
+
DoctypeMeta,
|
|
434
|
+
FetchMap,
|
|
435
|
+
FetchRule,
|
|
436
|
+
FormEngine,
|
|
437
|
+
FormTab,
|
|
438
|
+
LevelPerm,
|
|
439
|
+
PermLevelMatrix,
|
|
440
|
+
} from "./formEngine";
|
|
@@ -144,7 +144,10 @@ describe("SchemaForm design-law (MPO-60 / R10)", () => {
|
|
|
144
144
|
renderWithProviders(
|
|
145
145
|
<SchemaForm schema={schema} doc={{ first_name: "Jane" }} setValue={vi.fn()} />,
|
|
146
146
|
);
|
|
147
|
-
|
|
147
|
+
// `hidden` is a getByRole option, not a SelectorMatcherOptions one —
|
|
148
|
+
// getByLabelText never filters on visibility, so it was a no-op that only
|
|
149
|
+
// typechecked by accident (MPO-109).
|
|
150
|
+
const input = screen.getByLabelText("First Name") as HTMLInputElement;
|
|
148
151
|
expect(input).toBeTruthy();
|
|
149
152
|
expect(input.value).toBe("Jane");
|
|
150
153
|
const footer = input.closest("[aria-hidden='true']") as HTMLElement | null;
|