@multiplatform.one/frappe-ui 7.6.3 → 7.7.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/ChartCard.cjs +52 -0
- package/dist/cjs/ChartCard.native.js +72 -0
- package/dist/cjs/ChartCard.native.js.map +1 -0
- 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/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/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/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/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/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/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/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/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/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/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/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/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/FrappeList.stories.tsx +3 -10
- package/src/components/attachments.stories.tsx +9 -2
- package/src/dependsOn.ts +2 -2
- 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/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/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/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/FrappeDashboard.d.ts +2 -10
- package/types/FrappeDashboard.d.ts.map +1 -1
- package/types/dependsOn.d.ts +2 -2
- package/types/fields/CodeEditor/index.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/index.d.ts +4 -1
- package/types/index.d.ts.map +1 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desk form layout engine.
|
|
3
|
+
*
|
|
4
|
+
* Splits the flat DocType field list into desk tabs (Tab Break), sections
|
|
5
|
+
* (Section Break, collapsible honored), and columns (Column Break), and
|
|
6
|
+
* maps them to the generic `FormSchema` consumed by frappe-ui's SchemaForm.
|
|
7
|
+
*
|
|
8
|
+
* Dynamic behavior is computed HERE with the safe depends_on evaluator
|
|
9
|
+
* (dependsOn.ts) — the `depends_on` string is deliberately stripped from the
|
|
10
|
+
* FieldSchema handed to SchemaForm so its string-eval-based evaluator
|
|
11
|
+
* (which cannot run on Hermes/GJS) never engages:
|
|
12
|
+
*
|
|
13
|
+
* - depends_on → field omitted from the built schema
|
|
14
|
+
* - mandatory_depends_on → `required: true` on the built FieldSchema
|
|
15
|
+
* - read_only_depends_on → `readOnly: true` on the built FieldSchema
|
|
16
|
+
* - permlevel → hidden without level read, read-only without
|
|
17
|
+
* level write (role matrix via fieldPermissions.ts; coarse read-only
|
|
18
|
+
* fallback when roles are unavailable)
|
|
19
|
+
* - set_only_once → read-only on saved docs
|
|
20
|
+
* - fetch_from → read-only while the source link holds a value
|
|
21
|
+
* (unless fetch_if_empty)
|
|
22
|
+
* - docstatus > 0 → everything read-only except allow_on_submit
|
|
23
|
+
* fields on a submitted (1) doc
|
|
24
|
+
* - collapsible / collapsible_depends_on / missing-mandatory → per-section
|
|
25
|
+
* computed collapse state (desk refresh_section_collapse)
|
|
26
|
+
*
|
|
27
|
+
* Sections whose Section Break has a false depends_on hide wholesale; a
|
|
28
|
+
* section with no visible fields is dropped so SchemaForm never renders an
|
|
29
|
+
* empty card.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { formatCalendarDate } from "@multiplatform.one/theme";
|
|
33
|
+
import type { FieldSchema } from "../fieldRegistry";
|
|
34
|
+
import type { FormSchema, FormSection } from "../schemaForm";
|
|
35
|
+
import { evaluateDependsOn } from "../dependsOn";
|
|
36
|
+
import { canReadLevel, canWriteLevel, type PermLevelMatrix } from "./fieldPermissions";
|
|
37
|
+
import { isFetchLocked } from "./fetchFrom";
|
|
38
|
+
import type { DocFieldMeta } from "./doctypeMeta";
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Types
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export interface FormTab {
|
|
45
|
+
/** Stable key (fieldname of the Tab Break or "__details") */
|
|
46
|
+
key: string;
|
|
47
|
+
label: string;
|
|
48
|
+
schema: FormSchema;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface LayoutColumn {
|
|
52
|
+
fields: DocFieldMeta[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface LayoutSection {
|
|
56
|
+
breakField?: DocFieldMeta;
|
|
57
|
+
columns: LayoutColumn[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface LayoutTab {
|
|
61
|
+
key: string;
|
|
62
|
+
label?: string;
|
|
63
|
+
sections: LayoutSection[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Static structure (from meta, doc-independent)
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
function splitIntoTabs(fields: DocFieldMeta[]): LayoutTab[] {
|
|
71
|
+
const tabs: LayoutTab[] = [];
|
|
72
|
+
let currentTab: LayoutTab = { key: "__details", sections: [] };
|
|
73
|
+
let currentSection: LayoutSection = { columns: [{ fields: [] }] };
|
|
74
|
+
currentTab.sections.push(currentSection);
|
|
75
|
+
tabs.push(currentTab);
|
|
76
|
+
|
|
77
|
+
for (const field of fields) {
|
|
78
|
+
switch (field.fieldtype) {
|
|
79
|
+
case "Tab Break":
|
|
80
|
+
currentTab = { key: field.fieldname, label: field.label, sections: [] };
|
|
81
|
+
currentSection = { columns: [{ fields: [] }] };
|
|
82
|
+
currentTab.sections.push(currentSection);
|
|
83
|
+
tabs.push(currentTab);
|
|
84
|
+
break;
|
|
85
|
+
case "Section Break":
|
|
86
|
+
currentSection = { breakField: field, columns: [{ fields: [] }] };
|
|
87
|
+
currentTab.sections.push(currentSection);
|
|
88
|
+
break;
|
|
89
|
+
case "Column Break":
|
|
90
|
+
currentSection.columns.push({ fields: [] });
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
currentSection.columns[currentSection.columns.length - 1].fields.push(field);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return tabs;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Doc-dependent resolution
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
export interface BuildTabsOptions {
|
|
105
|
+
fields: DocFieldMeta[];
|
|
106
|
+
doc: Record<string, unknown>;
|
|
107
|
+
/** docstatus of the loaded doc (0 draft / 1 submitted / 2 cancelled). */
|
|
108
|
+
docstatus: number;
|
|
109
|
+
/** Meta-level read_only or missing write permission. */
|
|
110
|
+
formReadOnly: boolean;
|
|
111
|
+
/** Create mode relaxes read_only_depends_on on unsaved values. */
|
|
112
|
+
isCreate: boolean;
|
|
113
|
+
/** Default label for the implicit first tab. */
|
|
114
|
+
detailsLabel: string;
|
|
115
|
+
/**
|
|
116
|
+
* Role-derived permlevel matrix (fieldPermissions.ts). Desk semantics:
|
|
117
|
+
* fields at a level without read HIDE; read without write → read-only.
|
|
118
|
+
* Undefined (fixtures / roles unavailable) → coarse fallback: any
|
|
119
|
+
* permlevel > 0 renders read-only, never hidden.
|
|
120
|
+
*/
|
|
121
|
+
permLevels?: PermLevelMatrix;
|
|
122
|
+
/** Host-hidden fields (fieldnames) — dropped before layout resolution. */
|
|
123
|
+
hiddenFields?: readonly string[];
|
|
124
|
+
/**
|
|
125
|
+
* Host-hidden sections/tabs. Matches a Section Break's or Tab Break's
|
|
126
|
+
* fieldname OR label; the whole section (or tab) is dropped.
|
|
127
|
+
*/
|
|
128
|
+
hiddenSections?: readonly string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isFieldVisible(
|
|
132
|
+
field: DocFieldMeta,
|
|
133
|
+
doc: Record<string, unknown>,
|
|
134
|
+
permLevels?: PermLevelMatrix,
|
|
135
|
+
): boolean {
|
|
136
|
+
if (field.hidden) return false;
|
|
137
|
+
// Desk hides fields whose permlevel the user cannot read (perm.js:209-215).
|
|
138
|
+
if (!canReadLevel(permLevels, field.permlevel ?? 0)) return false;
|
|
139
|
+
// Visibility fallback = true: an unparseable expression shows the field.
|
|
140
|
+
return evaluateDependsOn(field.depends_on, doc, true);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toFieldSchema(
|
|
144
|
+
field: DocFieldMeta,
|
|
145
|
+
doc: Record<string, unknown>,
|
|
146
|
+
options: BuildTabsOptions,
|
|
147
|
+
): FieldSchema {
|
|
148
|
+
const required =
|
|
149
|
+
!!field.reqd ||
|
|
150
|
+
(!!field.mandatory_depends_on && evaluateDependsOn(field.mandatory_depends_on, doc, false));
|
|
151
|
+
|
|
152
|
+
let readOnly =
|
|
153
|
+
!!field.read_only ||
|
|
154
|
+
(!!field.read_only_depends_on && evaluateDependsOn(field.read_only_depends_on, doc, false)) ||
|
|
155
|
+
!canWriteLevel(options.permLevels, field.permlevel ?? 0);
|
|
156
|
+
// set_only_once: editable only while the doc is unsaved (perm.js:261-263).
|
|
157
|
+
if (field.set_only_once && !options.isCreate) readOnly = true;
|
|
158
|
+
// fetch_from without fetch_if_empty locks the target while its source link
|
|
159
|
+
// holds a value (desk read_only_because_of_fetch_from).
|
|
160
|
+
if (!readOnly && isFetchLocked(field, doc)) readOnly = true;
|
|
161
|
+
if (options.docstatus === 1 && !field.allow_on_submit) readOnly = true;
|
|
162
|
+
if (options.docstatus === 2) readOnly = true;
|
|
163
|
+
if (options.formReadOnly) readOnly = true;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
type: field.fieldtype,
|
|
167
|
+
name: field.fieldname,
|
|
168
|
+
label: field.label,
|
|
169
|
+
options: field.options,
|
|
170
|
+
description: field.description,
|
|
171
|
+
required,
|
|
172
|
+
readOnly,
|
|
173
|
+
precision:
|
|
174
|
+
field.precision !== undefined && field.precision !== null && field.precision !== ""
|
|
175
|
+
? Number(field.precision)
|
|
176
|
+
: undefined,
|
|
177
|
+
maxLength:
|
|
178
|
+
field.length !== undefined && field.length !== null && Number(field.length) > 0
|
|
179
|
+
? Number(field.length)
|
|
180
|
+
: undefined,
|
|
181
|
+
// depends_on intentionally NOT forwarded — visibility resolved here.
|
|
182
|
+
fieldtype: field.fieldtype,
|
|
183
|
+
fieldname: field.fieldname,
|
|
184
|
+
in_list_view: field.in_list_view,
|
|
185
|
+
in_standard_filter: field.in_standard_filter,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Desk's refresh_section_collapse (layout.js:463-483): a collapsible section
|
|
191
|
+
* starts collapsed unless its `collapsible_depends_on` evaluates truthy, and
|
|
192
|
+
* a section with a missing mandatory value is forced open. Recomputed per
|
|
193
|
+
* doc change — SchemaForm treats it as the live default while manual toggles
|
|
194
|
+
* survive between recomputes.
|
|
195
|
+
*/
|
|
196
|
+
function resolveSectionCollapsed(
|
|
197
|
+
breakField: DocFieldMeta | undefined,
|
|
198
|
+
sectionFields: DocFieldMeta[],
|
|
199
|
+
doc: Record<string, unknown>,
|
|
200
|
+
): boolean | undefined {
|
|
201
|
+
if (!breakField?.collapsible) return undefined;
|
|
202
|
+
let collapsed = true;
|
|
203
|
+
if (breakField.collapsible_depends_on) {
|
|
204
|
+
// Unparseable expression → expanded (never hide content silently).
|
|
205
|
+
collapsed = !evaluateDependsOn(breakField.collapsible_depends_on, doc, true);
|
|
206
|
+
}
|
|
207
|
+
if (collapsed && sectionHasMissingMandatory(sectionFields, doc)) collapsed = false;
|
|
208
|
+
return collapsed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Desk section.has_missing_mandatory: `reqd && value == null` (section.js:146-156). */
|
|
212
|
+
function sectionHasMissingMandatory(fields: DocFieldMeta[], doc: Record<string, unknown>): boolean {
|
|
213
|
+
return fields.some((field) => !!field.reqd && doc[field.fieldname] == null);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Build the desk tab list for the current doc state. Recomputed whenever
|
|
218
|
+
* the doc changes so depends_on chains re-resolve; the work is O(fields).
|
|
219
|
+
*/
|
|
220
|
+
export function buildFormTabs(options: BuildTabsOptions): FormTab[] {
|
|
221
|
+
const { fields, doc, detailsLabel, permLevels } = options;
|
|
222
|
+
const hiddenFieldSet = new Set(options.hiddenFields ?? []);
|
|
223
|
+
const hiddenSectionSet = new Set(options.hiddenSections ?? []);
|
|
224
|
+
const sectionHidden = (breakField: DocFieldMeta | undefined, label?: string): boolean => {
|
|
225
|
+
if (hiddenSectionSet.size === 0) return false;
|
|
226
|
+
if (breakField && hiddenSectionSet.has(breakField.fieldname)) return true;
|
|
227
|
+
const resolvedLabel = breakField?.label ?? label;
|
|
228
|
+
return !!resolvedLabel && hiddenSectionSet.has(resolvedLabel);
|
|
229
|
+
};
|
|
230
|
+
const layoutTabs = splitIntoTabs(fields);
|
|
231
|
+
const tabs: FormTab[] = [];
|
|
232
|
+
|
|
233
|
+
for (const layoutTab of layoutTabs) {
|
|
234
|
+
// Tab Break fields support depends_on + permlevel gating too (tab.js:38-43).
|
|
235
|
+
const tabBreak = fields.find(
|
|
236
|
+
(field) => field.fieldtype === "Tab Break" && field.fieldname === layoutTab.key,
|
|
237
|
+
);
|
|
238
|
+
if (tabBreak && !isFieldVisible(tabBreak, doc, permLevels)) continue;
|
|
239
|
+
// Host hiddenSections also match tabs (fieldname or label).
|
|
240
|
+
if (sectionHidden(tabBreak, layoutTab.label)) continue;
|
|
241
|
+
|
|
242
|
+
const sections: FormSection[] = [];
|
|
243
|
+
const flatFields: FieldSchema[] = [];
|
|
244
|
+
|
|
245
|
+
for (const section of layoutTab.sections) {
|
|
246
|
+
// Section Breaks are not permlevel-gated in desk (sections aren't
|
|
247
|
+
// controls; they only auto-hide when all their fields hide) — so the
|
|
248
|
+
// permlevel matrix is deliberately not passed here.
|
|
249
|
+
if (section.breakField && !isFieldVisible(section.breakField, doc)) continue;
|
|
250
|
+
if (sectionHidden(section.breakField)) continue;
|
|
251
|
+
|
|
252
|
+
const columns = section.columns
|
|
253
|
+
.map((column) => ({
|
|
254
|
+
fields: column.fields
|
|
255
|
+
.filter(
|
|
256
|
+
(field) =>
|
|
257
|
+
!hiddenFieldSet.has(field.fieldname) && isFieldVisible(field, doc, permLevels),
|
|
258
|
+
)
|
|
259
|
+
.map((field) => toFieldSchema(field, doc, options)),
|
|
260
|
+
}))
|
|
261
|
+
.filter((column) => column.fields.length > 0);
|
|
262
|
+
if (columns.length === 0) continue;
|
|
263
|
+
|
|
264
|
+
for (const column of columns) flatFields.push(...column.fields);
|
|
265
|
+
const sectionFields = section.columns.flatMap((column) => column.fields);
|
|
266
|
+
sections.push({
|
|
267
|
+
label: section.breakField?.label,
|
|
268
|
+
description: section.breakField?.description,
|
|
269
|
+
collapsible: !!section.breakField?.collapsible,
|
|
270
|
+
collapsed: resolveSectionCollapsed(section.breakField, sectionFields, doc),
|
|
271
|
+
hideBorder: !!section.breakField?.hide_border,
|
|
272
|
+
columns,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (sections.length === 0) continue;
|
|
277
|
+
tabs.push({
|
|
278
|
+
key: layoutTab.key,
|
|
279
|
+
label: layoutTab.label || detailsLabel,
|
|
280
|
+
schema: { sections, fields: flatFields },
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return tabs;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// Create-mode defaults
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
const LAYOUT_TYPES = new Set(["Section Break", "Column Break", "Tab Break", "Fold"]);
|
|
292
|
+
const NUMERIC_TYPES = new Set(["Int", "Float", "Currency", "Percent", "Check", "Rating"]);
|
|
293
|
+
|
|
294
|
+
const todayISO = () => formatCalendarDate(new Date());
|
|
295
|
+
const nowTime = () => formatCalendarDate(new Date(), "time");
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Seed a create-mode doc from meta field defaults — desk parity with
|
|
299
|
+
* create_new.js get_default_value / set_default_values:
|
|
300
|
+
*
|
|
301
|
+
* - `Today` → today's date; `now` → time (Time) / local datetime (else)
|
|
302
|
+
* - `__user` / `user` (Link/Data only, avoiding Select option collisions)
|
|
303
|
+
* → session user
|
|
304
|
+
* - Select fields with no default seed their first option
|
|
305
|
+
* (create_new.js:107-114)
|
|
306
|
+
* - `:fieldname` boot sysdefaults, `frappe.*`, `user_fullname` stay unset —
|
|
307
|
+
* server-resolved on insert (documented gap)
|
|
308
|
+
*/
|
|
309
|
+
export function buildCreateDefaults(
|
|
310
|
+
fields: DocFieldMeta[],
|
|
311
|
+
currentUser?: string,
|
|
312
|
+
): Record<string, unknown> {
|
|
313
|
+
const doc: Record<string, unknown> = {};
|
|
314
|
+
for (const field of fields) {
|
|
315
|
+
if (LAYOUT_TYPES.has(field.fieldtype)) continue;
|
|
316
|
+
const raw = field.default;
|
|
317
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
318
|
+
// Desk seeds the first Select option when no default resolves.
|
|
319
|
+
if (
|
|
320
|
+
field.fieldtype === "Select" &&
|
|
321
|
+
typeof field.options === "string" &&
|
|
322
|
+
field.options &&
|
|
323
|
+
!["[Select]", "Loading..."].includes(field.options)
|
|
324
|
+
) {
|
|
325
|
+
doc[field.fieldname] = field.options.split("\n")[0];
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (raw === "Today") {
|
|
330
|
+
doc[field.fieldname] = todayISO();
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (raw.toLowerCase() === "now") {
|
|
334
|
+
doc[field.fieldname] = field.fieldtype === "Time" ? nowTime() : `${todayISO()} ${nowTime()}`;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (
|
|
338
|
+
raw === "__user" ||
|
|
339
|
+
(raw.toLowerCase() === "user" && (field.fieldtype === "Link" || field.fieldtype === "Data"))
|
|
340
|
+
) {
|
|
341
|
+
if (currentUser) doc[field.fieldname] = currentUser;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (raw === "user_fullname") continue; // session fullname not mirrored client-side
|
|
345
|
+
if (raw.startsWith(":") || raw.startsWith("frappe.")) continue; // server-resolved
|
|
346
|
+
doc[field.fieldname] = NUMERIC_TYPES.has(field.fieldtype) ? Number(raw) || 0 : raw;
|
|
347
|
+
}
|
|
348
|
+
return doc;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Copyable field values for Duplicate / Amend: strips standard bookkeeping
|
|
353
|
+
* fields, `no_copy` fields, layout breaks, and empty values.
|
|
354
|
+
*/
|
|
355
|
+
export function buildCopyValues(
|
|
356
|
+
fields: DocFieldMeta[],
|
|
357
|
+
doc: Record<string, unknown>,
|
|
358
|
+
): Record<string, unknown> {
|
|
359
|
+
const copy: Record<string, unknown> = {};
|
|
360
|
+
for (const field of fields) {
|
|
361
|
+
if (LAYOUT_TYPES.has(field.fieldtype)) continue;
|
|
362
|
+
if (field.no_copy) continue;
|
|
363
|
+
if (field.fieldtype === "Attach" || field.fieldtype === "Attach Image") continue;
|
|
364
|
+
const value = doc[field.fieldname];
|
|
365
|
+
if (value === undefined || value === null || value === "") continue;
|
|
366
|
+
if (field.fieldtype === "Table" || field.fieldtype === "Table MultiSelect") {
|
|
367
|
+
if (Array.isArray(value)) {
|
|
368
|
+
copy[field.fieldname] = value.map((row) => {
|
|
369
|
+
const {
|
|
370
|
+
name: _name,
|
|
371
|
+
parent: _parent,
|
|
372
|
+
parentfield: _parentfield,
|
|
373
|
+
parenttype: _parenttype,
|
|
374
|
+
creation: _creation,
|
|
375
|
+
modified: _modified,
|
|
376
|
+
...rest
|
|
377
|
+
} = (row ?? {}) as Record<string, unknown>;
|
|
378
|
+
return rest;
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
copy[field.fieldname] = value;
|
|
384
|
+
}
|
|
385
|
+
return copy;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** True when the doctype names docs by user prompt (show a name input). */
|
|
389
|
+
export function isPromptNamed(autoname?: string, namingRule?: string): boolean {
|
|
390
|
+
if (namingRule === 'By "Prompt"') return true;
|
|
391
|
+
return (autoname ?? "").toLowerCase() === "prompt";
|
|
392
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The desk form engine.
|
|
3
|
+
*
|
|
4
|
+
* Frappe's desk resolves a form from meta at render time: tabs and sections
|
|
5
|
+
* from the layout breaks, visibility and read-only from `depends_on` chains,
|
|
6
|
+
* `permlevel`, `set_only_once`, `fetch_from`, `docstatus`, and a
|
|
7
|
+
* save/submit/cancel/amend lifecycle on top. All of that used to live in
|
|
8
|
+
* `@multiplatform.one/backoffice`, so an app author who imported the
|
|
9
|
+
* EXPORTED `FrappeForm` got a strictly weaker form than the desk rendered
|
|
10
|
+
* for the same doctype (MPO-15).
|
|
11
|
+
*
|
|
12
|
+
* It lives here now. `@multiplatform.one/backoffice` re-exports every name
|
|
13
|
+
* from its previous paths, so existing imports — barrel and
|
|
14
|
+
* `@multiplatform.one/backoffice/src/form/*` deep — keep resolving.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type { DocFieldMeta, DocLinkMeta, DocPermissionRule, DoctypeMeta } from "./doctypeMeta";
|
|
18
|
+
|
|
19
|
+
export { buildPermLevelMatrix, canReadLevel, canWriteLevel } from "./fieldPermissions";
|
|
20
|
+
export type { LevelPerm, PermLevelMatrix } from "./fieldPermissions";
|
|
21
|
+
|
|
22
|
+
export { applyFetchedValues, buildFetchMap, isFetchLocked } from "./fetchFrom";
|
|
23
|
+
export type { FetchMap, FetchRule } from "./fetchFrom";
|
|
24
|
+
|
|
25
|
+
export { computeDirtyFields, isEmptyValue, isSameValue } from "./dirtyTracking";
|
|
26
|
+
|
|
27
|
+
export { setCreatePrefill, takeCreatePrefill } from "./createPrefill";
|
|
28
|
+
|
|
29
|
+
export { buildCopyValues, buildCreateDefaults, buildFormTabs, isPromptNamed } from "./formLayout";
|
|
30
|
+
export type { BuildTabsOptions, FormTab } from "./formLayout";
|
|
31
|
+
|
|
32
|
+
export { useFormEngine } from "./useFormEngine";
|
|
33
|
+
export type { FormEngine } from "./useFormEngine";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPO-15 — the form-engine seam, from this side.
|
|
3
|
+
*
|
|
4
|
+
* The desk form engine used to live in `@multiplatform.one/backoffice`, one
|
|
5
|
+
* tier ABOVE this package, so `FrappeForm` could not reach it: an app author
|
|
6
|
+
* who imported the EXPORTED component got a strictly weaker form than the
|
|
7
|
+
* desk renders for the same doctype. The engine lives here now.
|
|
8
|
+
*
|
|
9
|
+
* Three things have to stay true for that to mean anything, and each one has
|
|
10
|
+
* broken silently in this repo before:
|
|
11
|
+
*
|
|
12
|
+
* 1. The engine is REACHABLE from the package barrel — a hoist that lands
|
|
13
|
+
* the files but leaves them unexported moves the problem, not the code.
|
|
14
|
+
* 2. Nothing under `public/frappe-ui/src` imports `@multiplatform.one/`
|
|
15
|
+
* `backoffice`. A lower tier importing a higher one is the defect the
|
|
16
|
+
* hoist exists to remove; re-introducing it here would be a cycle
|
|
17
|
+
* (backoffice already depends on this package).
|
|
18
|
+
* 3. The engine modules do not reach back up through the barrel either —
|
|
19
|
+
* `formLayout` takes its FieldSchema/FormSchema types from the sibling
|
|
20
|
+
* modules directly, so importing the engine never drags SchemaForm,
|
|
21
|
+
* FrappeTable and the whole table/native stack into a node-ish consumer.
|
|
22
|
+
*/
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { describe, expect, it } from "vitest";
|
|
26
|
+
import * as formEngine from "./index";
|
|
27
|
+
|
|
28
|
+
const engineDir = path.resolve(__dirname);
|
|
29
|
+
const srcDir = path.resolve(__dirname, "..");
|
|
30
|
+
|
|
31
|
+
function sourceFiles(dir: string): string[] {
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
const full = path.join(dir, entry.name);
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
out.push(...sourceFiles(full));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!/\.(ts|tsx)$/.test(entry.name)) continue;
|
|
40
|
+
out.push(full);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Every name the engine barrel publishes (values and types alike). */
|
|
46
|
+
const ENGINE_VALUES = [
|
|
47
|
+
"applyFetchedValues",
|
|
48
|
+
"buildCopyValues",
|
|
49
|
+
"buildCreateDefaults",
|
|
50
|
+
"buildFetchMap",
|
|
51
|
+
"buildFormTabs",
|
|
52
|
+
"buildPermLevelMatrix",
|
|
53
|
+
"canReadLevel",
|
|
54
|
+
"canWriteLevel",
|
|
55
|
+
"computeDirtyFields",
|
|
56
|
+
"isEmptyValue",
|
|
57
|
+
"isFetchLocked",
|
|
58
|
+
"isPromptNamed",
|
|
59
|
+
"isSameValue",
|
|
60
|
+
"setCreatePrefill",
|
|
61
|
+
"takeCreatePrefill",
|
|
62
|
+
"useFormEngine",
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
/** Types the engine barrel publishes alongside those values. */
|
|
66
|
+
const ENGINE_TYPES = [
|
|
67
|
+
"BuildTabsOptions",
|
|
68
|
+
"DocFieldMeta",
|
|
69
|
+
"DocLinkMeta",
|
|
70
|
+
"DocPermissionRule",
|
|
71
|
+
"DoctypeMeta",
|
|
72
|
+
"FetchMap",
|
|
73
|
+
"FetchRule",
|
|
74
|
+
"FormEngine",
|
|
75
|
+
"FormTab",
|
|
76
|
+
"LevelPerm",
|
|
77
|
+
"PermLevelMatrix",
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
describe("form-engine seam (MPO-15)", () => {
|
|
81
|
+
it("hands every engine value out of ./formEngine", () => {
|
|
82
|
+
for (const name of ENGINE_VALUES) {
|
|
83
|
+
expect(
|
|
84
|
+
(formEngine as Record<string, unknown>)[name],
|
|
85
|
+
`./formEngine must export "${name}"`,
|
|
86
|
+
).toBeTypeOf("function");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("re-exports the whole engine from the package barrel", () => {
|
|
91
|
+
// Source-level, not a runtime import of ../index: pulling the full barrel
|
|
92
|
+
// in here would drag FrappeTable -> the table/forms stacks into what is
|
|
93
|
+
// otherwise a node-ish spec, for no extra signal (tests/exports.spec.ts
|
|
94
|
+
// already exercises the barrel at runtime).
|
|
95
|
+
const barrel = fs.readFileSync(path.resolve(srcDir, "index.ts"), "utf-8");
|
|
96
|
+
const block = barrel.slice(barrel.indexOf("Desk Form Engine"));
|
|
97
|
+
expect(block, "the barrel must carry a form-engine block").toContain('from "./formEngine"');
|
|
98
|
+
for (const name of [...ENGINE_VALUES, ...ENGINE_TYPES]) {
|
|
99
|
+
expect(block, `the barrel must re-export "${name}"`).toContain(name);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("keeps formActions off both barrels (deep-import only, colliding names)", () => {
|
|
104
|
+
// `addComment`, `addTag`, `getCount`, `sendEmail` and friends collide with
|
|
105
|
+
// the list and shell surfaces; the dedicated "./formActions" subpath
|
|
106
|
+
// export is how a consumer reaches them.
|
|
107
|
+
for (const name of ["addComment", "addTag", "getCount", "sendEmail", "renameDoc"]) {
|
|
108
|
+
expect((formEngine as Record<string, unknown>)[name], name).toBeUndefined();
|
|
109
|
+
}
|
|
110
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(srcDir, "../package.json"), "utf-8")) as {
|
|
111
|
+
exports: Record<string, { source?: string }>;
|
|
112
|
+
};
|
|
113
|
+
expect(pkg.exports["./formActions"]?.source).toBe("./src/formEngine/formActions.ts");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("never imports the package that sits above it", () => {
|
|
117
|
+
for (const file of sourceFiles(srcDir)) {
|
|
118
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
119
|
+
expect(source, file).not.toMatch(/from ["']@multiplatform\.one\/backoffice/);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("keeps the engine off its own package barrel (no import cycle)", () => {
|
|
124
|
+
for (const file of sourceFiles(engineDir)) {
|
|
125
|
+
if (file.includes(".spec.")) continue;
|
|
126
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
127
|
+
expect(source, file).not.toMatch(/from ["']@multiplatform\.one\/frappe-ui/);
|
|
128
|
+
expect(source, file).not.toMatch(/from ["']\.\.\/index["']/);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("keeps the meta shapes here, where the engine that consumes them lives", () => {
|
|
133
|
+
const meta = fs.readFileSync(path.resolve(engineDir, "doctypeMeta.ts"), "utf-8");
|
|
134
|
+
for (const name of ["DocFieldMeta", "DocLinkMeta", "DocPermissionRule", "DoctypeMeta"]) {
|
|
135
|
+
expect(meta, name).toContain(`export interface ${name}`);
|
|
136
|
+
}
|
|
137
|
+
// The shapes reach the engine by relative import, never through the barrel.
|
|
138
|
+
for (const file of [
|
|
139
|
+
"formLayout.ts",
|
|
140
|
+
"fetchFrom.ts",
|
|
141
|
+
"fieldPermissions.ts",
|
|
142
|
+
"useFormEngine.ts",
|
|
143
|
+
]) {
|
|
144
|
+
const source = fs.readFileSync(path.resolve(engineDir, file), "utf-8");
|
|
145
|
+
expect(source, file).not.toMatch(/shared\/useDoctypeMeta/);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|