@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,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desk form RPC helpers — thin wrappers over the whitelisted methods the
|
|
3
|
+
* stock desk form view calls. Every payload here was probed against a live
|
|
4
|
+
* bench (frappe v15) before implementation:
|
|
5
|
+
*
|
|
6
|
+
* frappe.desk.form.assign_to.add {doctype, name, assign_to: [..], description?}
|
|
7
|
+
* frappe.desk.form.assign_to.remove {doctype, name, assign_to: "user"}
|
|
8
|
+
* frappe.desk.form.utils.add_comment {reference_doctype, reference_name, content,
|
|
9
|
+
* comment_email, comment_by}
|
|
10
|
+
* frappe.desk.doctype.tag.tag.add_tag {tag, dt, dn}
|
|
11
|
+
* frappe.desk.doctype.tag.tag.remove_tag{tag, dt, dn}
|
|
12
|
+
* frappe.desk.like.toggle_like {doctype, name, add: "Yes"|"No"}
|
|
13
|
+
* frappe.desk.form.document_follow.follow_document / unfollow_document
|
|
14
|
+
* {doctype, doc_name, user}
|
|
15
|
+
* frappe.model.rename_doc.update_document_title
|
|
16
|
+
* {doctype, docname, name, merge: 0} → new name
|
|
17
|
+
* (what desk calls — syncs the title field too)
|
|
18
|
+
* frappe.client.submit {doc} (full doc incl. modified — OCC enforced)
|
|
19
|
+
* frappe.client.cancel {doctype, name}
|
|
20
|
+
* frappe.client.get_count {doctype, filters}
|
|
21
|
+
* frappe.client.validate_link {doctype, docname, fields: [..]} →
|
|
22
|
+
* {name, <field>: value} (fetch_from preview)
|
|
23
|
+
* frappe.core.doctype.communication.email.make
|
|
24
|
+
* {doctype, name, recipients, subject, content,
|
|
25
|
+
* send_email: 1}
|
|
26
|
+
* frappe.desk.form.utils.remove_attach {fid, dt, dn}
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { HttpClient, FileModule, type AuthConfig } from "@multiplatform.one/frappe";
|
|
30
|
+
import type { FixtureProvider } from "@multiplatform.one/frappe";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Connection shape shared by every form loader/action — structurally
|
|
34
|
+
* compatible with frappe-ui's FrappeConfig and the frappe package's
|
|
35
|
+
* UseFrappeCollectionConfig (function tokens included).
|
|
36
|
+
*/
|
|
37
|
+
export interface FrappeConnection {
|
|
38
|
+
baseURL: string;
|
|
39
|
+
socketPort?: number;
|
|
40
|
+
/** Same-origin socket override (see FrappeConfig.socketBaseURL). */
|
|
41
|
+
socketBaseURL?: string;
|
|
42
|
+
/** Frappe site name — the realtime Socket.IO namespace. */
|
|
43
|
+
siteName?: string;
|
|
44
|
+
auth?: {
|
|
45
|
+
useToken?: boolean;
|
|
46
|
+
token?: string | (() => string | Promise<string>);
|
|
47
|
+
type?: string;
|
|
48
|
+
};
|
|
49
|
+
// biome-ignore lint: matches FrappeConfig.syncOptions
|
|
50
|
+
syncOptions?: any;
|
|
51
|
+
fixtures?: FixtureProvider;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createHttpClient(connection: FrappeConnection): HttpClient {
|
|
55
|
+
const auth: AuthConfig | undefined = connection.auth
|
|
56
|
+
? {
|
|
57
|
+
useToken: connection.auth.useToken,
|
|
58
|
+
token: connection.auth.token,
|
|
59
|
+
type: connection.auth.type as AuthConfig["type"],
|
|
60
|
+
}
|
|
61
|
+
: undefined;
|
|
62
|
+
return new HttpClient(connection.baseURL, auth);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function post<T = unknown>(
|
|
66
|
+
http: HttpClient,
|
|
67
|
+
method: string,
|
|
68
|
+
body: Record<string, unknown>,
|
|
69
|
+
): Promise<T | undefined> {
|
|
70
|
+
const response = await http.post<{ message?: T }>(`/api/method/${method}`, body);
|
|
71
|
+
return response.payload?.message;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Assignments
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export function addAssignment(
|
|
79
|
+
http: HttpClient,
|
|
80
|
+
doctype: string,
|
|
81
|
+
name: string,
|
|
82
|
+
users: string[],
|
|
83
|
+
description?: string,
|
|
84
|
+
) {
|
|
85
|
+
return post(http, "frappe.desk.form.assign_to.add", {
|
|
86
|
+
doctype,
|
|
87
|
+
name,
|
|
88
|
+
assign_to: users,
|
|
89
|
+
description: description || undefined,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function removeAssignment(http: HttpClient, doctype: string, name: string, user: string) {
|
|
94
|
+
return post(http, "frappe.desk.form.assign_to.remove", { doctype, name, assign_to: user });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Comments
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
export function addComment(
|
|
102
|
+
http: HttpClient,
|
|
103
|
+
args: {
|
|
104
|
+
doctype: string;
|
|
105
|
+
name: string;
|
|
106
|
+
content: string;
|
|
107
|
+
commentEmail: string;
|
|
108
|
+
commentBy?: string;
|
|
109
|
+
},
|
|
110
|
+
) {
|
|
111
|
+
return post(http, "frappe.desk.form.utils.add_comment", {
|
|
112
|
+
reference_doctype: args.doctype,
|
|
113
|
+
reference_name: args.name,
|
|
114
|
+
content: args.content,
|
|
115
|
+
comment_email: args.commentEmail,
|
|
116
|
+
comment_by: args.commentBy ?? args.commentEmail,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Tags
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export function addTag(http: HttpClient, doctype: string, name: string, tag: string) {
|
|
125
|
+
return post(http, "frappe.desk.doctype.tag.tag.add_tag", { tag, dt: doctype, dn: name });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function removeTag(http: HttpClient, doctype: string, name: string, tag: string) {
|
|
129
|
+
return post(http, "frappe.desk.doctype.tag.tag.remove_tag", { tag, dt: doctype, dn: name });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Like / Follow
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
export function toggleLike(http: HttpClient, doctype: string, name: string, add: boolean) {
|
|
137
|
+
return post(http, "frappe.desk.like.toggle_like", {
|
|
138
|
+
doctype,
|
|
139
|
+
name,
|
|
140
|
+
add: add ? "Yes" : "No",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function followDocument(http: HttpClient, doctype: string, name: string, user: string) {
|
|
145
|
+
return post(http, "frappe.desk.form.document_follow.follow_document", {
|
|
146
|
+
doctype,
|
|
147
|
+
doc_name: name,
|
|
148
|
+
user,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function unfollowDocument(http: HttpClient, doctype: string, name: string, user: string) {
|
|
153
|
+
return post(http, "frappe.desk.form.document_follow.unfollow_document", {
|
|
154
|
+
doctype,
|
|
155
|
+
doc_name: name,
|
|
156
|
+
user,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Document lifecycle
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Rename a document; resolves with the new name. Uses desk's own
|
|
166
|
+
* `update_document_title` (handles title-field sync; the bare
|
|
167
|
+
* `frappe.rename_doc` does not). Pass a `title` argument instead of `name`
|
|
168
|
+
* to retitle without renaming — not needed here yet.
|
|
169
|
+
*/
|
|
170
|
+
export async function renameDoc(
|
|
171
|
+
http: HttpClient,
|
|
172
|
+
doctype: string,
|
|
173
|
+
oldName: string,
|
|
174
|
+
newName: string,
|
|
175
|
+
): Promise<string> {
|
|
176
|
+
const result = await post<string>(http, "frappe.model.rename_doc.update_document_title", {
|
|
177
|
+
doctype,
|
|
178
|
+
docname: oldName,
|
|
179
|
+
name: newName,
|
|
180
|
+
merge: 0,
|
|
181
|
+
});
|
|
182
|
+
return result ?? newName;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Submit a saved draft (docstatus 0 → 1). Requires the current full doc for OCC. */
|
|
186
|
+
export function submitDoc(http: HttpClient, doc: Record<string, unknown>) {
|
|
187
|
+
return post<Record<string, unknown>>(http, "frappe.client.submit", { doc });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Cancel a submitted doc (docstatus 1 → 2). */
|
|
191
|
+
export function cancelDoc(http: HttpClient, doctype: string, name: string) {
|
|
192
|
+
return post<Record<string, unknown>>(http, "frappe.client.cancel", { doctype, name });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Direct REST insert — returns the created doc (with its server-issued name). */
|
|
196
|
+
export async function insertDoc(
|
|
197
|
+
http: HttpClient,
|
|
198
|
+
doctype: string,
|
|
199
|
+
values: Record<string, unknown>,
|
|
200
|
+
): Promise<Record<string, unknown>> {
|
|
201
|
+
const response = await http.post<{ data?: Record<string, unknown> }>(
|
|
202
|
+
`/api/resource/${encodeURIComponent(doctype)}`,
|
|
203
|
+
values,
|
|
204
|
+
);
|
|
205
|
+
const data = response.payload?.data;
|
|
206
|
+
if (!data) throw new Error(`Insert into ${doctype} returned no document`);
|
|
207
|
+
return data;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate a link value and fetch columns off the linked doc in one call —
|
|
212
|
+
* what desk's Link control uses to drive fetch_from previews. Resolves with
|
|
213
|
+
* `{name, <field>: value, ...}`; rejects when the target doc is missing.
|
|
214
|
+
*/
|
|
215
|
+
export async function validateLink(
|
|
216
|
+
http: HttpClient,
|
|
217
|
+
doctype: string,
|
|
218
|
+
docname: string,
|
|
219
|
+
fields: string[],
|
|
220
|
+
): Promise<Record<string, unknown>> {
|
|
221
|
+
const result = await post<Record<string, unknown>>(http, "frappe.client.validate_link", {
|
|
222
|
+
doctype,
|
|
223
|
+
docname,
|
|
224
|
+
fields,
|
|
225
|
+
});
|
|
226
|
+
return result ?? {};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Misc
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
export async function getCount(
|
|
234
|
+
http: HttpClient,
|
|
235
|
+
doctype: string,
|
|
236
|
+
filters: Record<string, unknown>,
|
|
237
|
+
): Promise<number> {
|
|
238
|
+
const response = await http.get<{ message?: number }>("/api/method/frappe.client.get_count", {
|
|
239
|
+
doctype,
|
|
240
|
+
filters: JSON.stringify(filters),
|
|
241
|
+
});
|
|
242
|
+
return response.payload?.message ?? 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let cachedUser: string | undefined;
|
|
246
|
+
export async function getLoggedUser(http: HttpClient): Promise<string> {
|
|
247
|
+
if (cachedUser) return cachedUser;
|
|
248
|
+
const response = await http.get<{ message?: string }>("/api/method/frappe.auth.get_logged_user");
|
|
249
|
+
cachedUser = response.payload?.message ?? "Guest";
|
|
250
|
+
return cachedUser;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function removeAttachment(http: HttpClient, fileId: string, doctype: string, name: string) {
|
|
254
|
+
return post(http, "frappe.desk.form.utils.remove_attach", { fid: fileId, dt: doctype, dn: name });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function uploadAttachment(
|
|
258
|
+
http: HttpClient,
|
|
259
|
+
file: File | Blob,
|
|
260
|
+
doctype: string,
|
|
261
|
+
name: string,
|
|
262
|
+
): Promise<unknown> {
|
|
263
|
+
const files = new FileModule(http);
|
|
264
|
+
return files.uploadFile(file, { doctype, docname: name });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function sendEmail(
|
|
268
|
+
http: HttpClient,
|
|
269
|
+
args: {
|
|
270
|
+
doctype: string;
|
|
271
|
+
name: string;
|
|
272
|
+
recipients: string;
|
|
273
|
+
subject: string;
|
|
274
|
+
content: string;
|
|
275
|
+
},
|
|
276
|
+
) {
|
|
277
|
+
return post(http, "frappe.core.doctype.communication.email.make", {
|
|
278
|
+
doctype: args.doctype,
|
|
279
|
+
name: args.name,
|
|
280
|
+
recipients: args.recipients,
|
|
281
|
+
subject: args.subject,
|
|
282
|
+
content: args.content,
|
|
283
|
+
send_email: 1,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* formLayout — desk layout-engine parity specs. Ground truth:
|
|
3
|
+
* frappe/public/js/frappe/form/layout.js (+ section.js, tab.js) and
|
|
4
|
+
* frappe/public/js/frappe/model/perm.js get_field_display_status.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import type { DocFieldMeta } from "./doctypeMeta";
|
|
8
|
+
import { buildPermLevelMatrix } from "./fieldPermissions";
|
|
9
|
+
import { buildCreateDefaults, buildFormTabs, type BuildTabsOptions } from "./formLayout";
|
|
10
|
+
|
|
11
|
+
function field(overrides: Partial<DocFieldMeta> & { fieldname: string }): DocFieldMeta {
|
|
12
|
+
return { fieldtype: "Data", label: overrides.fieldname, ...overrides };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function build(
|
|
16
|
+
fields: DocFieldMeta[],
|
|
17
|
+
overrides: Partial<BuildTabsOptions> = {},
|
|
18
|
+
): ReturnType<typeof buildFormTabs> {
|
|
19
|
+
return buildFormTabs({
|
|
20
|
+
fields,
|
|
21
|
+
doc: {},
|
|
22
|
+
docstatus: 0,
|
|
23
|
+
formReadOnly: false,
|
|
24
|
+
isCreate: false,
|
|
25
|
+
detailsLabel: "Details",
|
|
26
|
+
...overrides,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function flatFieldNames(tabs: ReturnType<typeof buildFormTabs>): string[] {
|
|
31
|
+
return tabs.flatMap((tab) => tab.schema.fields.map((f) => f.name));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findField(tabs: ReturnType<typeof buildFormTabs>, name: string) {
|
|
35
|
+
for (const tab of tabs) {
|
|
36
|
+
const hit = tab.schema.fields.find((f) => f.name === name);
|
|
37
|
+
if (hit) return hit;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("tab / section / column grouping", () => {
|
|
43
|
+
it("groups fields under Tab Breaks with an implicit Details tab", () => {
|
|
44
|
+
const tabs = build([
|
|
45
|
+
field({ fieldname: "a" }),
|
|
46
|
+
field({ fieldname: "tab1", fieldtype: "Tab Break", label: "More" }),
|
|
47
|
+
field({ fieldname: "b" }),
|
|
48
|
+
]);
|
|
49
|
+
expect(tabs.map((t) => t.key)).toEqual(["__details", "tab1"]);
|
|
50
|
+
expect(tabs[0].label).toBe("Details");
|
|
51
|
+
expect(tabs[0].schema.fields.map((f) => f.name)).toEqual(["a"]);
|
|
52
|
+
expect(tabs[1].schema.fields.map((f) => f.name)).toEqual(["b"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("drops the implicit Details tab when the doctype starts with a Tab Break (User)", () => {
|
|
56
|
+
const tabs = build([
|
|
57
|
+
field({ fieldname: "details_tab", fieldtype: "Tab Break", label: "User Details" }),
|
|
58
|
+
field({ fieldname: "email" }),
|
|
59
|
+
]);
|
|
60
|
+
expect(tabs.map((t) => t.key)).toEqual(["details_tab"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("splits Section Breaks into sections and Column Breaks into columns", () => {
|
|
64
|
+
const tabs = build([
|
|
65
|
+
field({ fieldname: "a" }),
|
|
66
|
+
field({ fieldname: "sec1", fieldtype: "Section Break", label: "Second" }),
|
|
67
|
+
field({ fieldname: "b" }),
|
|
68
|
+
field({ fieldname: "col1", fieldtype: "Column Break" }),
|
|
69
|
+
field({ fieldname: "c" }),
|
|
70
|
+
]);
|
|
71
|
+
const sections = tabs[0].schema.sections;
|
|
72
|
+
expect(sections).toHaveLength(2);
|
|
73
|
+
expect(sections[1].label).toBe("Second");
|
|
74
|
+
expect(sections[1].columns).toHaveLength(2);
|
|
75
|
+
expect(sections[1].columns[0].fields[0].name).toBe("b");
|
|
76
|
+
expect(sections[1].columns[1].fields[0].name).toBe("c");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("hidden + depends_on visibility", () => {
|
|
81
|
+
it("drops hidden fields", () => {
|
|
82
|
+
const tabs = build([field({ fieldname: "a" }), field({ fieldname: "b", hidden: 1 })]);
|
|
83
|
+
expect(flatFieldNames(tabs)).toEqual(["a"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("auto-hides a section when all its fields hide (desk empty-section)", () => {
|
|
87
|
+
const tabs = build([
|
|
88
|
+
field({ fieldname: "a" }),
|
|
89
|
+
field({ fieldname: "sec1", fieldtype: "Section Break", label: "Hidden Section" }),
|
|
90
|
+
field({ fieldname: "b", hidden: 1 }),
|
|
91
|
+
field({ fieldname: "c", depends_on: "eval:doc.missing" }),
|
|
92
|
+
]);
|
|
93
|
+
expect(tabs[0].schema.sections).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("auto-hides a tab when all its sections hide", () => {
|
|
97
|
+
const tabs = build([
|
|
98
|
+
field({ fieldname: "a" }),
|
|
99
|
+
field({ fieldname: "tab1", fieldtype: "Tab Break", label: "Empty" }),
|
|
100
|
+
field({ fieldname: "b", hidden: 1 }),
|
|
101
|
+
]);
|
|
102
|
+
expect(tabs.map((t) => t.key)).toEqual(["__details"]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("hides a whole section when its Section Break depends_on is false", () => {
|
|
106
|
+
const tabs = build(
|
|
107
|
+
[
|
|
108
|
+
field({ fieldname: "a" }),
|
|
109
|
+
field({
|
|
110
|
+
fieldname: "sec1",
|
|
111
|
+
fieldtype: "Section Break",
|
|
112
|
+
depends_on: "eval:doc.show_more",
|
|
113
|
+
}),
|
|
114
|
+
field({ fieldname: "b" }),
|
|
115
|
+
],
|
|
116
|
+
{ doc: { show_more: 0 } },
|
|
117
|
+
);
|
|
118
|
+
expect(flatFieldNames(tabs)).toEqual(["a"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("re-resolves depends_on against the live doc", () => {
|
|
122
|
+
const fields = [
|
|
123
|
+
field({ fieldname: "channel", fieldtype: "Select", options: "Email\nSlack" }),
|
|
124
|
+
field({
|
|
125
|
+
fieldname: "slack_webhook_url",
|
|
126
|
+
depends_on: "eval:doc.channel=='Slack'",
|
|
127
|
+
mandatory_depends_on: "eval:doc.channel=='Slack'",
|
|
128
|
+
}),
|
|
129
|
+
];
|
|
130
|
+
expect(flatFieldNames(build(fields, { doc: { channel: "Email" } }))).toEqual(["channel"]);
|
|
131
|
+
const slackTabs = build(fields, { doc: { channel: "Slack" } });
|
|
132
|
+
expect(flatFieldNames(slackTabs)).toEqual(["channel", "slack_webhook_url"]);
|
|
133
|
+
expect(findField(slackTabs, "slack_webhook_url")?.required).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("read_only / read_only_depends_on / set_only_once", () => {
|
|
138
|
+
it("marks read_only fields readOnly", () => {
|
|
139
|
+
const tabs = build([field({ fieldname: "a", read_only: 1 })]);
|
|
140
|
+
expect(findField(tabs, "a")?.readOnly).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("resolves read_only_depends_on per doc state", () => {
|
|
144
|
+
const fields = [field({ fieldname: "a", read_only_depends_on: "eval:doc.locked" })];
|
|
145
|
+
expect(findField(build(fields, { doc: { locked: 1 } }), "a")?.readOnly).toBe(true);
|
|
146
|
+
expect(findField(build(fields, { doc: { locked: 0 } }), "a")?.readOnly).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("set_only_once: editable on new docs, read-only on saved docs (perm.js:261)", () => {
|
|
150
|
+
const fields = [field({ fieldname: "a", set_only_once: 1 })];
|
|
151
|
+
expect(findField(build(fields, { isCreate: true }), "a")?.readOnly).toBe(false);
|
|
152
|
+
expect(findField(build(fields, { isCreate: false }), "a")?.readOnly).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("docstatus lifecycle locking", () => {
|
|
157
|
+
const fields = [
|
|
158
|
+
field({ fieldname: "locked_field" }),
|
|
159
|
+
field({ fieldname: "open_field", allow_on_submit: 1 }),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
it("docstatus 1 locks everything except allow_on_submit", () => {
|
|
163
|
+
const tabs = build(fields, { docstatus: 1 });
|
|
164
|
+
expect(findField(tabs, "locked_field")?.readOnly).toBe(true);
|
|
165
|
+
expect(findField(tabs, "open_field")?.readOnly).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("docstatus 2 locks allow_on_submit fields too", () => {
|
|
169
|
+
const tabs = build(fields, { docstatus: 2 });
|
|
170
|
+
expect(findField(tabs, "open_field")?.readOnly).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("permlevel gating (perm.js get_field_display_status)", () => {
|
|
175
|
+
const permissions = [
|
|
176
|
+
{ role: "System Manager", permlevel: 0, read: 1 as const, write: 1 as const },
|
|
177
|
+
{ role: "System Manager", permlevel: 1, read: 1 as const, write: 1 as const },
|
|
178
|
+
{ role: "Auditor", permlevel: 1, read: 1 as const, write: 0 as const },
|
|
179
|
+
{ role: "Desk User", permlevel: 0, read: 1 as const, write: 1 as const },
|
|
180
|
+
];
|
|
181
|
+
const fields = [field({ fieldname: "plain" }), field({ fieldname: "restricted", permlevel: 1 })];
|
|
182
|
+
|
|
183
|
+
it("write at the level → editable (fixes the coarse always-read-only rule)", () => {
|
|
184
|
+
const matrix = buildPermLevelMatrix(permissions, ["System Manager"]);
|
|
185
|
+
const tabs = build(fields, { permLevels: matrix });
|
|
186
|
+
expect(findField(tabs, "restricted")?.readOnly).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("read without write at the level → read-only", () => {
|
|
190
|
+
const matrix = buildPermLevelMatrix(permissions, ["Auditor"]);
|
|
191
|
+
const tabs = build(fields, { permLevels: matrix });
|
|
192
|
+
expect(findField(tabs, "restricted")?.readOnly).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("no read at the level → field hidden (desk status None)", () => {
|
|
196
|
+
const matrix = buildPermLevelMatrix(permissions, ["Desk User"]);
|
|
197
|
+
const tabs = build(fields, { permLevels: matrix });
|
|
198
|
+
expect(findField(tabs, "restricted")).toBeUndefined();
|
|
199
|
+
expect(findField(tabs, "plain")).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("without a matrix falls back to coarse permlevel>0 read-only, never hidden", () => {
|
|
203
|
+
const tabs = build(fields);
|
|
204
|
+
expect(findField(tabs, "restricted")?.readOnly).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("hides a Tab Break the user cannot read (tab.js:41)", () => {
|
|
208
|
+
const matrix = buildPermLevelMatrix(permissions, ["Desk User"]);
|
|
209
|
+
const tabs = build(
|
|
210
|
+
[
|
|
211
|
+
field({ fieldname: "a" }),
|
|
212
|
+
field({ fieldname: "sec_tab", fieldtype: "Tab Break", label: "Secret", permlevel: 1 }),
|
|
213
|
+
field({ fieldname: "b" }),
|
|
214
|
+
],
|
|
215
|
+
{ permLevels: matrix },
|
|
216
|
+
);
|
|
217
|
+
expect(tabs.map((t) => t.key)).toEqual(["__details"]);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("section collapse state (layout.js refresh_section_collapse)", () => {
|
|
222
|
+
it("collapsible sections start collapsed", () => {
|
|
223
|
+
const tabs = build([
|
|
224
|
+
field({ fieldname: "sec1", fieldtype: "Section Break", label: "S", collapsible: 1 }),
|
|
225
|
+
field({ fieldname: "a" }),
|
|
226
|
+
]);
|
|
227
|
+
expect(tabs[0].schema.sections[0].collapsed).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("collapsible_depends_on truthy → expanded (Notification print settings)", () => {
|
|
231
|
+
const fields = [
|
|
232
|
+
field({
|
|
233
|
+
fieldname: "sec1",
|
|
234
|
+
fieldtype: "Section Break",
|
|
235
|
+
label: "S",
|
|
236
|
+
collapsible: 1,
|
|
237
|
+
collapsible_depends_on: "attach_print",
|
|
238
|
+
}),
|
|
239
|
+
field({ fieldname: "a" }),
|
|
240
|
+
];
|
|
241
|
+
expect(build(fields, { doc: { attach_print: 1 } })[0].schema.sections[0].collapsed).toBe(false);
|
|
242
|
+
expect(build(fields, { doc: { attach_print: 0 } })[0].schema.sections[0].collapsed).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("a missing mandatory value forces the section open (section.js has_missing_mandatory)", () => {
|
|
246
|
+
const fields = [
|
|
247
|
+
field({ fieldname: "sec1", fieldtype: "Section Break", label: "S", collapsible: 1 }),
|
|
248
|
+
field({ fieldname: "a", reqd: 1 }),
|
|
249
|
+
];
|
|
250
|
+
expect(build(fields, { doc: {} })[0].schema.sections[0].collapsed).toBe(false);
|
|
251
|
+
// Desk checks `value == null` — an empty string counts as present.
|
|
252
|
+
expect(build(fields, { doc: { a: "" } })[0].schema.sections[0].collapsed).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("non-collapsible sections have no collapse state", () => {
|
|
256
|
+
const tabs = build([
|
|
257
|
+
field({ fieldname: "sec1", fieldtype: "Section Break", label: "S" }),
|
|
258
|
+
field({ fieldname: "a" }),
|
|
259
|
+
]);
|
|
260
|
+
expect(tabs[0].schema.sections[0].collapsed).toBeUndefined();
|
|
261
|
+
expect(tabs[0].schema.sections[0].collapsible).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("forwards hide_border and description", () => {
|
|
265
|
+
const tabs = build([
|
|
266
|
+
field({
|
|
267
|
+
fieldname: "sec1",
|
|
268
|
+
fieldtype: "Section Break",
|
|
269
|
+
label: "S",
|
|
270
|
+
hide_border: 1,
|
|
271
|
+
description: "About this group",
|
|
272
|
+
}),
|
|
273
|
+
field({ fieldname: "a" }),
|
|
274
|
+
]);
|
|
275
|
+
expect(tabs[0].schema.sections[0].hideBorder).toBe(true);
|
|
276
|
+
expect(tabs[0].schema.sections[0].description).toBe("About this group");
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("fetch_from read-only lock", () => {
|
|
281
|
+
const fields = [
|
|
282
|
+
field({ fieldname: "customer", fieldtype: "Link", options: "Customer" }),
|
|
283
|
+
field({ fieldname: "customer_name", fetch_from: "customer.customer_name" }),
|
|
284
|
+
field({
|
|
285
|
+
fieldname: "note",
|
|
286
|
+
fetch_from: "customer.note",
|
|
287
|
+
fetch_if_empty: 1,
|
|
288
|
+
}),
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
it("locks a fetch target while its source link is set (base_input.js:56)", () => {
|
|
292
|
+
const tabs = build(fields, { doc: { customer: "CUST-1" } });
|
|
293
|
+
expect(findField(tabs, "customer_name")?.readOnly).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("keeps the target editable when the source is empty or fetch_if_empty is set", () => {
|
|
297
|
+
const emptyTabs = build(fields, { doc: {} });
|
|
298
|
+
expect(findField(emptyTabs, "customer_name")?.readOnly).toBe(false);
|
|
299
|
+
const setTabs = build(fields, { doc: { customer: "CUST-1" } });
|
|
300
|
+
expect(findField(setTabs, "note")?.readOnly).toBe(false);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("field property pass-through", () => {
|
|
305
|
+
it("forwards description, precision, and length", () => {
|
|
306
|
+
const tabs = build([
|
|
307
|
+
field({ fieldname: "a", description: "help", precision: "3", length: 20 }),
|
|
308
|
+
]);
|
|
309
|
+
const schema = findField(tabs, "a");
|
|
310
|
+
expect(schema?.description).toBe("help");
|
|
311
|
+
expect(schema?.precision).toBe(3);
|
|
312
|
+
expect(schema?.maxLength).toBe(20);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("buildCreateDefaults (create_new.js get_default_value)", () => {
|
|
317
|
+
it("seeds static, Today, and __user defaults", () => {
|
|
318
|
+
const doc = buildCreateDefaults(
|
|
319
|
+
[
|
|
320
|
+
field({
|
|
321
|
+
fieldname: "status",
|
|
322
|
+
fieldtype: "Select",
|
|
323
|
+
options: "Open\nClosed",
|
|
324
|
+
default: "Open",
|
|
325
|
+
}),
|
|
326
|
+
field({ fieldname: "date", fieldtype: "Date", default: "Today" }),
|
|
327
|
+
field({ fieldname: "owner_field", fieldtype: "Link", options: "User", default: "__user" }),
|
|
328
|
+
field({ fieldname: "count", fieldtype: "Int", default: "5" }),
|
|
329
|
+
],
|
|
330
|
+
"admin@example.com",
|
|
331
|
+
);
|
|
332
|
+
expect(doc.status).toBe("Open");
|
|
333
|
+
expect(doc.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
334
|
+
expect(doc.owner_field).toBe("admin@example.com");
|
|
335
|
+
expect(doc.count).toBe(5);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("seeds `user` alias on Link/Data only and `now` per fieldtype", () => {
|
|
339
|
+
const doc = buildCreateDefaults(
|
|
340
|
+
[
|
|
341
|
+
field({ fieldname: "who", fieldtype: "Link", options: "User", default: "user" }),
|
|
342
|
+
field({ fieldname: "sel", fieldtype: "Select", options: "user\nrole", default: "user" }),
|
|
343
|
+
field({ fieldname: "at_time", fieldtype: "Time", default: "now" }),
|
|
344
|
+
field({ fieldname: "at_dt", fieldtype: "Datetime", default: "now" }),
|
|
345
|
+
],
|
|
346
|
+
"admin@example.com",
|
|
347
|
+
);
|
|
348
|
+
expect(doc.who).toBe("admin@example.com");
|
|
349
|
+
expect(doc.sel).toBe("user"); // Select keeps its literal option
|
|
350
|
+
expect(doc.at_time).toMatch(/^\d{2}:\d{2}:\d{2}$/);
|
|
351
|
+
expect(doc.at_dt).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("seeds the first Select option when no default exists (create_new.js:107)", () => {
|
|
355
|
+
const doc = buildCreateDefaults([
|
|
356
|
+
field({ fieldname: "kind", fieldtype: "Select", options: "Small\nLarge" }),
|
|
357
|
+
field({ fieldname: "empty_first", fieldtype: "Select", options: "\nA\nB" }),
|
|
358
|
+
field({ fieldname: "placeholder", fieldtype: "Select", options: "[Select]" }),
|
|
359
|
+
]);
|
|
360
|
+
expect(doc.kind).toBe("Small");
|
|
361
|
+
expect(doc.empty_first).toBe("");
|
|
362
|
+
expect(doc.placeholder).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("leaves server-resolved tokens unset", () => {
|
|
366
|
+
const doc = buildCreateDefaults([
|
|
367
|
+
field({ fieldname: "company", fieldtype: "Link", options: "Company", default: ":Company" }),
|
|
368
|
+
field({ fieldname: "fullname", fieldtype: "Data", default: "user_fullname" }),
|
|
369
|
+
]);
|
|
370
|
+
expect(doc.company).toBeUndefined();
|
|
371
|
+
expect(doc.fullname).toBeUndefined();
|
|
372
|
+
});
|
|
373
|
+
});
|