@multiplatform.one/frappe 7.10.0 → 7.15.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.
Files changed (239) hide show
  1. package/README.md +59 -1
  2. package/dist/cjs/auth.native.js.map +1 -1
  3. package/dist/cjs/call.native.js.map +1 -1
  4. package/dist/cjs/collection.native.js.map +1 -1
  5. package/dist/cjs/common/types.native.js.map +1 -1
  6. package/dist/cjs/core/http.native.js.map +1 -1
  7. package/dist/cjs/core/types.native.js.map +1 -1
  8. package/dist/cjs/core/wire.native.js.map +1 -1
  9. package/dist/cjs/db.native.js.map +1 -1
  10. package/dist/cjs/devtools/FrappeDevtoolsPanel.native.js.map +1 -1
  11. package/dist/cjs/devtools/index.native.js.map +1 -1
  12. package/dist/cjs/devtools/plugin.native.js.map +1 -1
  13. package/dist/cjs/devtools/theme.native.js.map +1 -1
  14. package/dist/cjs/doctype.cjs +25 -43
  15. package/dist/cjs/doctype.native.js +27 -43
  16. package/dist/cjs/doctype.native.js.map +1 -1
  17. package/dist/cjs/file.native.js.map +1 -1
  18. package/dist/cjs/fixtureDb.native.js.map +1 -1
  19. package/dist/cjs/fixtures.cjs +3 -0
  20. package/dist/cjs/fixtures.native.js +3 -0
  21. package/dist/cjs/fixtures.native.js.map +1 -1
  22. package/dist/cjs/frappeApp.native.js.map +1 -1
  23. package/dist/cjs/frappeReact.cjs +67 -31
  24. package/dist/cjs/frappeReact.native.js +87 -34
  25. package/dist/cjs/frappeReact.native.js.map +1 -1
  26. package/dist/cjs/index.cjs +7 -0
  27. package/dist/cjs/index.native.js +9 -2
  28. package/dist/cjs/index.native.js.map +1 -1
  29. package/dist/cjs/optimistic.native.js.map +1 -1
  30. package/dist/cjs/pacer.native.js.map +1 -1
  31. package/dist/cjs/plugin/index.native.js.map +1 -1
  32. package/dist/cjs/realtime.native.js.map +1 -1
  33. package/dist/cjs/serverLoaders.cjs +74 -0
  34. package/dist/cjs/serverLoaders.native.js +88 -0
  35. package/dist/cjs/serverLoaders.native.js.map +1 -0
  36. package/dist/cjs/sync/backfill.native.js.map +1 -1
  37. package/dist/cjs/sync/filters.native.js.map +1 -1
  38. package/dist/cjs/sync/index.cjs +99 -14
  39. package/dist/cjs/sync/index.native.js +129 -14
  40. package/dist/cjs/sync/index.native.js.map +1 -1
  41. package/dist/cjs/sync/merge.native.js.map +1 -1
  42. package/dist/cjs/sync/mutationEngine.native.js.map +1 -1
  43. package/dist/cjs/sync/mutationJournal.native.js.map +1 -1
  44. package/dist/cjs/sync/normalizedCache.native.js.map +1 -1
  45. package/dist/cjs/sync/persistence.native.js.map +1 -1
  46. package/dist/cjs/sync/realtime.native.js.map +1 -1
  47. package/dist/cjs/sync/rehydration.cjs +4 -0
  48. package/dist/cjs/sync/rehydration.native.js +6 -0
  49. package/dist/cjs/sync/rehydration.native.js.map +1 -1
  50. package/dist/cjs/sync/storage.native.js.map +1 -1
  51. package/dist/cjs/sync/subscriptionManager.native.js.map +1 -1
  52. package/dist/cjs/sync/types.native.js.map +1 -1
  53. package/dist/cjs/sync/virtualPageManager.native.js.map +1 -1
  54. package/dist/cjs/syncOptionsKey.cjs +59 -0
  55. package/dist/cjs/syncOptionsKey.native.js +82 -0
  56. package/dist/cjs/syncOptionsKey.native.js.map +1 -0
  57. package/dist/cjs/useLiveQuery.cjs +2 -20
  58. package/dist/cjs/useLiveQuery.native.js +1 -22
  59. package/dist/cjs/useLiveQuery.native.js.map +1 -1
  60. package/dist/cjs/util.native.js.map +1 -1
  61. package/dist/esm/auth.native.js.map +1 -1
  62. package/dist/esm/call.native.js.map +1 -1
  63. package/dist/esm/collection.native.js.map +1 -1
  64. package/dist/esm/common/types.native.js.map +1 -1
  65. package/dist/esm/core/http.native.js.map +1 -1
  66. package/dist/esm/core/types.native.js.map +1 -1
  67. package/dist/esm/core/wire.native.js.map +1 -1
  68. package/dist/esm/db.native.js.map +1 -1
  69. package/dist/esm/devtools/FrappeDevtoolsPanel.native.js.map +1 -1
  70. package/dist/esm/devtools/index.native.js.map +1 -1
  71. package/dist/esm/devtools/plugin.native.js.map +1 -1
  72. package/dist/esm/devtools/theme.native.js.map +1 -1
  73. package/dist/esm/doctype.mjs +23 -44
  74. package/dist/esm/doctype.mjs.map +1 -1
  75. package/dist/esm/doctype.native.js +25 -44
  76. package/dist/esm/doctype.native.js.map +1 -1
  77. package/dist/esm/file.native.js.map +1 -1
  78. package/dist/esm/fixtureDb.native.js.map +1 -1
  79. package/dist/esm/fixtures.mjs +3 -0
  80. package/dist/esm/fixtures.mjs.map +1 -1
  81. package/dist/esm/fixtures.native.js +3 -0
  82. package/dist/esm/fixtures.native.js.map +1 -1
  83. package/dist/esm/frappeApp.native.js.map +1 -1
  84. package/dist/esm/frappeReact.mjs +68 -33
  85. package/dist/esm/frappeReact.mjs.map +1 -1
  86. package/dist/esm/frappeReact.native.js +87 -35
  87. package/dist/esm/frappeReact.native.js.map +1 -1
  88. package/dist/esm/index.mjs +4 -3
  89. package/dist/esm/index.mjs.map +1 -1
  90. package/dist/esm/index.native.js +4 -3
  91. package/dist/esm/index.native.js.map +1 -1
  92. package/dist/esm/optimistic.native.js.map +1 -1
  93. package/dist/esm/pacer.native.js.map +1 -1
  94. package/dist/esm/plugin/index.native.js.map +1 -1
  95. package/dist/esm/realtime.native.js.map +1 -1
  96. package/dist/esm/serverLoaders.mjs +48 -0
  97. package/dist/esm/serverLoaders.mjs.map +1 -0
  98. package/dist/esm/serverLoaders.native.js +59 -0
  99. package/dist/esm/serverLoaders.native.js.map +1 -0
  100. package/dist/esm/sync/backfill.native.js.map +1 -1
  101. package/dist/esm/sync/filters.native.js.map +1 -1
  102. package/dist/esm/sync/index.mjs +99 -14
  103. package/dist/esm/sync/index.mjs.map +1 -1
  104. package/dist/esm/sync/index.native.js +129 -14
  105. package/dist/esm/sync/index.native.js.map +1 -1
  106. package/dist/esm/sync/merge.native.js.map +1 -1
  107. package/dist/esm/sync/mutationEngine.native.js.map +1 -1
  108. package/dist/esm/sync/mutationJournal.native.js.map +1 -1
  109. package/dist/esm/sync/normalizedCache.native.js.map +1 -1
  110. package/dist/esm/sync/persistence.native.js.map +1 -1
  111. package/dist/esm/sync/realtime.native.js.map +1 -1
  112. package/dist/esm/sync/rehydration.mjs +4 -0
  113. package/dist/esm/sync/rehydration.mjs.map +1 -1
  114. package/dist/esm/sync/rehydration.native.js +6 -0
  115. package/dist/esm/sync/rehydration.native.js.map +1 -1
  116. package/dist/esm/sync/storage.native.js.map +1 -1
  117. package/dist/esm/sync/subscriptionManager.native.js.map +1 -1
  118. package/dist/esm/sync/types.mjs.map +1 -1
  119. package/dist/esm/sync/types.native.js.map +1 -1
  120. package/dist/esm/sync/virtualPageManager.native.js.map +1 -1
  121. package/dist/esm/syncOptionsKey.mjs +34 -0
  122. package/dist/esm/syncOptionsKey.mjs.map +1 -0
  123. package/dist/esm/syncOptionsKey.native.js +54 -0
  124. package/dist/esm/syncOptionsKey.native.js.map +1 -0
  125. package/dist/esm/useLiveQuery.mjs +1 -19
  126. package/dist/esm/useLiveQuery.mjs.map +1 -1
  127. package/dist/esm/useLiveQuery.native.js +1 -22
  128. package/dist/esm/useLiveQuery.native.js.map +1 -1
  129. package/dist/esm/util.native.js.map +1 -1
  130. package/dist/jsx/auth.native.js.map +1 -1
  131. package/dist/jsx/call.native.js.map +1 -1
  132. package/dist/jsx/collection.native.js.map +1 -1
  133. package/dist/jsx/common/types.native.js.map +1 -1
  134. package/dist/jsx/core/http.native.js.map +1 -1
  135. package/dist/jsx/core/types.native.js.map +1 -1
  136. package/dist/jsx/core/wire.native.js.map +1 -1
  137. package/dist/jsx/db.native.js.map +1 -1
  138. package/dist/jsx/devtools/FrappeDevtoolsPanel.native.js.map +1 -1
  139. package/dist/jsx/devtools/index.native.js.map +1 -1
  140. package/dist/jsx/devtools/plugin.native.js.map +1 -1
  141. package/dist/jsx/devtools/theme.native.js.map +1 -1
  142. package/dist/jsx/doctype.mjs +23 -44
  143. package/dist/jsx/doctype.mjs.map +1 -1
  144. package/dist/jsx/doctype.native.js +27 -43
  145. package/dist/jsx/doctype.native.js.map +1 -1
  146. package/dist/jsx/file.native.js.map +1 -1
  147. package/dist/jsx/fixtureDb.native.js.map +1 -1
  148. package/dist/jsx/fixtures.mjs +3 -0
  149. package/dist/jsx/fixtures.mjs.map +1 -1
  150. package/dist/jsx/fixtures.native.js +3 -0
  151. package/dist/jsx/fixtures.native.js.map +1 -1
  152. package/dist/jsx/frappeApp.native.js.map +1 -1
  153. package/dist/jsx/frappeReact.mjs +68 -33
  154. package/dist/jsx/frappeReact.mjs.map +1 -1
  155. package/dist/jsx/frappeReact.native.js +87 -34
  156. package/dist/jsx/frappeReact.native.js.map +1 -1
  157. package/dist/jsx/index.js +4 -3
  158. package/dist/jsx/index.js.map +1 -1
  159. package/dist/jsx/index.mjs +4 -3
  160. package/dist/jsx/index.mjs.map +1 -1
  161. package/dist/jsx/index.native.js +9 -2
  162. package/dist/jsx/index.native.js.map +1 -1
  163. package/dist/jsx/optimistic.native.js.map +1 -1
  164. package/dist/jsx/pacer.native.js.map +1 -1
  165. package/dist/jsx/plugin/index.native.js.map +1 -1
  166. package/dist/jsx/realtime.native.js.map +1 -1
  167. package/dist/jsx/serverLoaders.mjs +48 -0
  168. package/dist/jsx/serverLoaders.mjs.map +1 -0
  169. package/dist/jsx/serverLoaders.native.js +88 -0
  170. package/dist/jsx/serverLoaders.native.js.map +1 -0
  171. package/dist/jsx/sync/backfill.native.js.map +1 -1
  172. package/dist/jsx/sync/filters.native.js.map +1 -1
  173. package/dist/jsx/sync/index.mjs +99 -14
  174. package/dist/jsx/sync/index.mjs.map +1 -1
  175. package/dist/jsx/sync/index.native.js +129 -14
  176. package/dist/jsx/sync/index.native.js.map +1 -1
  177. package/dist/jsx/sync/merge.native.js.map +1 -1
  178. package/dist/jsx/sync/mutationEngine.native.js.map +1 -1
  179. package/dist/jsx/sync/mutationJournal.native.js.map +1 -1
  180. package/dist/jsx/sync/normalizedCache.native.js.map +1 -1
  181. package/dist/jsx/sync/persistence.native.js.map +1 -1
  182. package/dist/jsx/sync/realtime.native.js.map +1 -1
  183. package/dist/jsx/sync/rehydration.mjs +4 -0
  184. package/dist/jsx/sync/rehydration.mjs.map +1 -1
  185. package/dist/jsx/sync/rehydration.native.js +6 -0
  186. package/dist/jsx/sync/rehydration.native.js.map +1 -1
  187. package/dist/jsx/sync/storage.native.js.map +1 -1
  188. package/dist/jsx/sync/subscriptionManager.native.js.map +1 -1
  189. package/dist/jsx/sync/types.mjs.map +1 -1
  190. package/dist/jsx/sync/types.native.js.map +1 -1
  191. package/dist/jsx/sync/virtualPageManager.native.js.map +1 -1
  192. package/dist/jsx/syncOptionsKey.mjs +34 -0
  193. package/dist/jsx/syncOptionsKey.mjs.map +1 -0
  194. package/dist/jsx/syncOptionsKey.native.js +82 -0
  195. package/dist/jsx/syncOptionsKey.native.js.map +1 -0
  196. package/dist/jsx/useLiveQuery.mjs +1 -19
  197. package/dist/jsx/useLiveQuery.mjs.map +1 -1
  198. package/dist/jsx/useLiveQuery.native.js +1 -22
  199. package/dist/jsx/useLiveQuery.native.js.map +1 -1
  200. package/dist/jsx/util.native.js.map +1 -1
  201. package/package.json +12 -12
  202. package/src/doctype.spec.ts +148 -5
  203. package/src/doctype.ts +202 -173
  204. package/src/fixtures.ts +1 -0
  205. package/src/frappeReact.catchUp.spec.tsx +105 -0
  206. package/src/frappeReact.hydration.spec.tsx +164 -0
  207. package/src/frappeReact.spec.ts +56 -0
  208. package/src/frappeReact.ts +93 -43
  209. package/src/index.ts +14 -0
  210. package/src/serverLoaders.ts +77 -0
  211. package/src/sync/catchUp.spec.ts +142 -0
  212. package/src/sync/index.ts +106 -9
  213. package/src/sync/rehydration.ts +5 -0
  214. package/src/sync/syncModule.spec.ts +20 -0
  215. package/src/sync/types.ts +9 -1
  216. package/src/syncOptionsKey.spec.ts +46 -0
  217. package/src/syncOptionsKey.ts +45 -0
  218. package/src/useLiveQuery.hydration.spec.tsx +129 -0
  219. package/src/useLiveQuery.spec.ts +3 -11
  220. package/src/useLiveQuery.ts +3 -57
  221. package/types/doctype.d.ts +117 -33
  222. package/types/doctype.d.ts.map +1 -1
  223. package/types/fixtures.d.ts.map +1 -1
  224. package/types/frappeReact.d.ts +9 -3
  225. package/types/frappeReact.d.ts.map +1 -1
  226. package/types/index.d.ts +6 -5
  227. package/types/index.d.ts.map +1 -1
  228. package/types/serverLoaders.d.ts +40 -0
  229. package/types/serverLoaders.d.ts.map +1 -0
  230. package/types/sync/index.d.ts +15 -1
  231. package/types/sync/index.d.ts.map +1 -1
  232. package/types/sync/rehydration.d.ts +2 -0
  233. package/types/sync/rehydration.d.ts.map +1 -1
  234. package/types/sync/types.d.ts +8 -1
  235. package/types/sync/types.d.ts.map +1 -1
  236. package/types/syncOptionsKey.d.ts +3 -0
  237. package/types/syncOptionsKey.d.ts.map +1 -0
  238. package/types/useLiveQuery.d.ts +3 -13
  239. package/types/useLiveQuery.d.ts.map +1 -1
package/src/doctype.ts CHANGED
@@ -5,95 +5,160 @@
5
5
  * documents, and searching across DocTypes for navigation (Awesomebar).
6
6
  */
7
7
 
8
+ import { getCookie } from "@multiplatform.one/platform";
8
9
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
10
  import { HttpClient } from "./core/http";
10
- import type { AuthConfig } from "./core/types";
11
+ import { FrappeError, type AuthConfig } from "./core/types";
11
12
  import type { FrappeConnectionConfig } from "./pacer";
12
13
 
13
14
  // ---------------------------------------------------------------------------
14
15
  // Type Definitions
15
16
  // ---------------------------------------------------------------------------
16
17
 
17
- /** All Frappe field type strings */
18
- export type FrappeFieldType =
19
- | "Autocomplete"
20
- | "Attach"
21
- | "Attach Image"
22
- | "Barcode"
23
- | "Button"
24
- | "Check"
25
- | "Code"
26
- | "Color"
27
- | "Column Break"
28
- | "Currency"
29
- | "Data"
30
- | "Date"
31
- | "Datetime"
32
- | "Duration"
33
- | "Dynamic Link"
34
- | "Float"
35
- | "Geolocation"
36
- | "HTML"
37
- | "HTML Editor"
38
- | "Heading"
39
- | "Icon"
40
- | "Image"
41
- | "Int"
42
- | "JSON"
43
- | "Link"
44
- | "Long Text"
45
- | "Markdown Editor"
46
- | "Password"
47
- | "Percent"
48
- | "Phone"
49
- | "Rating"
50
- | "Read Only"
51
- | "Section Break"
52
- | "Select"
53
- | "Signature"
54
- | "Small Text"
55
- | "Tab Break"
56
- | "Table"
57
- | "Table MultiSelect"
58
- | "Text"
59
- | "Text Editor"
60
- | "Time";
61
-
62
- /** Normalized DocType field descriptor */
18
+ /**
19
+ * Every fieldtype a Frappe bench serves: the options of `DocField.fieldtype`.
20
+ */
21
+ export const stockFieldTypes = [
22
+ "Autocomplete",
23
+ "Attach",
24
+ "Attach Image",
25
+ "Barcode",
26
+ "Button",
27
+ "Check",
28
+ "Code",
29
+ "Color",
30
+ "Column Break",
31
+ "Currency",
32
+ "Data",
33
+ "Date",
34
+ "Datetime",
35
+ "Duration",
36
+ "Dynamic Link",
37
+ "Float",
38
+ "Fold",
39
+ "Geolocation",
40
+ "HTML",
41
+ "HTML Editor",
42
+ "Heading",
43
+ "Icon",
44
+ "Image",
45
+ "Int",
46
+ "JSON",
47
+ "Link",
48
+ "Long Text",
49
+ "Markdown Editor",
50
+ "Password",
51
+ "Percent",
52
+ "Phone",
53
+ "Rating",
54
+ "Read Only",
55
+ "Section Break",
56
+ "Select",
57
+ "Signature",
58
+ "Small Text",
59
+ "Tab Break",
60
+ "Table",
61
+ "Table MultiSelect",
62
+ "Text",
63
+ "Text Editor",
64
+ "Time",
65
+ ] as const;
66
+
67
+ /**
68
+ * Fieldtypes no bench serves. frappe-ui's field registry renders them for
69
+ * hand-written meta, so they belong to the union: the registry must never
70
+ * accept a fieldtype the meta model forbids (MPO-15).
71
+ */
72
+ export const customFieldTypes = [
73
+ "Date Range",
74
+ "Mention",
75
+ "OTP",
76
+ "Progress",
77
+ "Radio",
78
+ "Slider",
79
+ "Switch",
80
+ "Toggle Group",
81
+ ] as const;
82
+
83
+ /** Every fieldtype a `DocTypeField` may carry, and all `registerField` accepts. */
84
+ export const frappeFieldTypes = [...stockFieldTypes, ...customFieldTypes] as const;
85
+
86
+ export type FrappeFieldType = (typeof frappeFieldTypes)[number];
87
+
88
+ /**
89
+ * One DocField row as `frappe.desk.form.load.getdoctype` sends it. This is
90
+ * the only field model: the desk form engine, the list surfaces and
91
+ * `useDocTypeMeta` all read it. Flags stay the server's `0 | 1`, and the
92
+ * index signature carries the keys no surface reads yet.
93
+ */
63
94
  export interface DocTypeField {
64
95
  fieldname: string;
65
96
  fieldtype: FrappeFieldType;
66
97
  label?: string;
67
98
  /** Link target DocType, Select options, or Table child DocType */
68
99
  options?: string;
69
- reqd?: boolean;
70
- unique?: boolean;
71
- default?: string;
100
+ reqd?: 0 | 1;
101
+ unique?: 0 | 1;
102
+ hidden?: 0 | 1;
103
+ read_only?: 0 | 1;
104
+ bold?: 0 | 1;
105
+ in_list_view?: 0 | 1;
106
+ in_standard_filter?: 0 | 1;
107
+ in_global_search?: 0 | 1;
108
+ allow_in_quick_entry?: 0 | 1;
109
+ allow_on_submit?: 0 | 1;
110
+ no_copy?: 0 | 1;
111
+ set_only_once?: 0 | 1;
112
+ report_hide?: 0 | 1;
113
+ /** For Section Break fields: whether the section is collapsible */
114
+ collapsible?: 0 | 1;
115
+ collapsible_depends_on?: string;
72
116
  /** Conditional visibility expression (e.g. `eval:doc.status=="Active"`) */
73
117
  depends_on?: string;
74
- read_only?: boolean;
75
- hidden?: boolean;
76
- in_list_view?: boolean;
77
- in_standard_filter?: boolean;
78
- description?: string;
118
+ mandatory_depends_on?: string;
119
+ read_only_depends_on?: string;
120
+ fetch_from?: string;
121
+ fetch_if_empty?: 0 | 1;
122
+ permlevel?: number;
79
123
  length?: number;
80
- precision?: number;
124
+ precision?: string | number;
125
+ default?: string;
126
+ description?: string;
81
127
  /** For Link fields: comma-separated search fields */
82
128
  search_fields?: string;
83
- /** For Section Break fields: whether the section is collapsible */
84
- collapsible?: boolean;
129
+ idx?: number;
130
+ [key: string]: unknown;
85
131
  }
86
132
 
87
- /** DocType permission entry */
133
+ /** Role permission row from the DocType meta. */
88
134
  export interface DocTypePermission {
89
- role: string;
90
- read?: boolean;
91
- write?: boolean;
92
- create?: boolean;
93
- delete?: boolean;
94
- submit?: boolean;
95
- cancel?: boolean;
96
- amend?: boolean;
135
+ role?: string;
136
+ permlevel?: number;
137
+ read?: 0 | 1;
138
+ write?: 0 | 1;
139
+ create?: 0 | 1;
140
+ delete?: 0 | 1;
141
+ submit?: 0 | 1;
142
+ cancel?: 0 | 1;
143
+ amend?: 0 | 1;
144
+ print?: 0 | 1;
145
+ email?: 0 | 1;
146
+ share?: 0 | 1;
147
+ if_owner?: 0 | 1;
148
+ [key: string]: unknown;
149
+ }
150
+
151
+ /** DocType Link child row (Connections tab). Probed: `group` arrives null when unset. */
152
+ export interface DocTypeLink {
153
+ name?: string;
154
+ link_doctype: string;
155
+ link_fieldname: string;
156
+ group?: string | null;
157
+ hidden?: 0 | 1;
158
+ is_child_table?: 0 | 1;
159
+ parent_doctype?: string;
160
+ table_fieldname?: string;
161
+ [key: string]: unknown;
97
162
  }
98
163
 
99
164
  /** A column in a form section, containing an ordered list of fields */
@@ -110,20 +175,48 @@ export interface FormSection {
110
175
  columns: FormColumn[];
111
176
  }
112
177
 
113
- /** Normalized DocType metadata */
178
+ /** DocType meta as `frappe.desk.form.load.getdoctype` sends it (loose: the server is the source of truth). */
114
179
  export interface DocTypeMeta {
180
+ doctype?: string;
115
181
  name: string;
116
- module: string;
182
+ module?: string;
183
+ issingle?: 0 | 1;
184
+ istable?: 0 | 1;
185
+ is_submittable?: 0 | 1;
186
+ is_tree?: 0 | 1;
187
+ /** Desk treeview_settings asset (`{doctype}_tree.js`) — gates Tree. */
188
+ __tree_js?: string;
189
+ /** Desk calendar_settings asset (`{doctype}_calendar.js`) — gates Calendar. */
190
+ __calendar_js?: string;
191
+ is_virtual?: 0 | 1;
192
+ editable_grid?: 0 | 1;
193
+ track_changes?: 0 | 1;
194
+ quick_entry?: 0 | 1;
195
+ autoname?: string;
196
+ naming_rule?: string;
197
+ allow_rename?: 0 | 1;
198
+ allow_copy?: 0 | 1;
199
+ read_only?: 0 | 1;
200
+ in_create?: 0 | 1;
201
+ hide_toolbar?: 0 | 1;
202
+ max_attachments?: number;
117
203
  title_field?: string;
118
204
  image_field?: string;
119
- is_tree?: boolean;
120
- sort_field: string;
121
- sort_order: string;
122
- /** Flat list of all fields (ordered by field_order or position) */
205
+ search_fields?: string;
206
+ sort_field?: string;
207
+ sort_order?: string;
208
+ icon?: string;
209
+ document_type?: string;
210
+ /** May arrive as a JSON string or an already-parsed array. */
211
+ field_order?: string[] | string;
212
+ /** Flat list of all fields (`useDocTypeMeta` orders them by `field_order`) */
123
213
  fields: DocTypeField[];
124
- /** Parsed layout tree from Section Break / Column Break / Tab Break fields */
125
- sections: FormSection[];
126
- permissions: DocTypePermission[];
214
+ permissions?: DocTypePermission[];
215
+ links?: DocTypeLink[];
216
+ states?: Array<{ title?: string; color?: string } & Record<string, unknown>>;
217
+ /** Layout tree parsed from the break fields. `useDocTypeMeta` fills it; a raw getdoctype doc has none. */
218
+ sections?: FormSection[];
219
+ [key: string]: unknown;
127
220
  }
128
221
 
129
222
  /** A search result for a Link field */
@@ -191,54 +284,6 @@ interface DocTypeListItem {
191
284
  title?: string;
192
285
  }
193
286
 
194
- // ---------------------------------------------------------------------------
195
- // Raw API response types (internal)
196
- // ---------------------------------------------------------------------------
197
-
198
- interface RawDocTypeResponse {
199
- name: string;
200
- module: string;
201
- title_field?: string;
202
- image_field?: string;
203
- is_tree?: number;
204
- sort_field?: string;
205
- sort_order?: string;
206
- field_order?: string[] | string;
207
- fields: RawFieldResponse[];
208
- permissions?: RawPermissionResponse[];
209
- }
210
-
211
- interface RawFieldResponse {
212
- fieldname: string;
213
- fieldtype: string;
214
- label?: string;
215
- options?: string;
216
- reqd?: number;
217
- unique?: number;
218
- default?: string;
219
- depends_on?: string;
220
- read_only?: number;
221
- hidden?: number;
222
- in_list_view?: number;
223
- in_standard_filter?: number;
224
- description?: string;
225
- length?: number;
226
- precision?: number;
227
- search_fields?: string;
228
- collapsible?: number;
229
- }
230
-
231
- interface RawPermissionResponse {
232
- role: string;
233
- read?: number;
234
- write?: number;
235
- create?: number;
236
- delete?: number;
237
- submit?: number;
238
- cancel?: number;
239
- amend?: number;
240
- }
241
-
242
287
  // ---------------------------------------------------------------------------
243
288
  // Utility: parseDependsOn
244
289
  // ---------------------------------------------------------------------------
@@ -328,7 +373,7 @@ export function parseFormLayout(fields: DocTypeField[]): FormSection[] {
328
373
  case "Section Break":
329
374
  currentSection = {
330
375
  label: field.label,
331
- collapsible: field.collapsible,
376
+ collapsible: !!field.collapsible,
332
377
  columns: [{ fields: [] }],
333
378
  };
334
379
  sections.push(currentSection);
@@ -384,39 +429,18 @@ function createHttpClient(config: FrappeConnectionConfig): HttpClient | undefine
384
429
  return new HttpClient(config.baseURL, authConfig);
385
430
  }
386
431
 
387
- function parseRawField(raw: RawFieldResponse): DocTypeField {
388
- return {
389
- fieldname: raw.fieldname,
390
- fieldtype: raw.fieldtype as FrappeFieldType,
391
- label: raw.label,
392
- options: raw.options,
393
- reqd: !!raw.reqd,
394
- unique: !!raw.unique,
395
- default: raw.default,
396
- depends_on: raw.depends_on,
397
- read_only: !!raw.read_only,
398
- hidden: !!raw.hidden,
399
- in_list_view: !!raw.in_list_view,
400
- in_standard_filter: !!raw.in_standard_filter,
401
- description: raw.description,
402
- length: raw.length,
403
- precision: raw.precision,
404
- search_fields: raw.search_fields,
405
- collapsible: !!raw.collapsible,
406
- };
407
- }
408
-
409
- function parseRawPermission(raw: RawPermissionResponse): DocTypePermission {
410
- return {
411
- role: raw.role,
412
- read: !!raw.read,
413
- write: !!raw.write,
414
- create: !!raw.create,
415
- delete: !!raw.delete,
416
- submit: !!raw.submit,
417
- cancel: !!raw.cancel,
418
- amend: !!raw.amend,
419
- };
432
+ /**
433
+ * True when requests on this connection reach Frappe as Guest: it names a
434
+ * token source, the source resolves empty (signed out), and no Frappe desk
435
+ * session cookie names a user. A cookie-only connection names no token
436
+ * source, so it is never judged here.
437
+ */
438
+ async function connectsAsGuest(auth: FrappeConnectionConfig["auth"]): Promise<boolean> {
439
+ if (!auth?.useToken || !auth.token) return false;
440
+ const token = typeof auth.token === "function" ? await auth.token() : auth.token;
441
+ if (token) return false;
442
+ const user = getCookie("user_id");
443
+ return !user || user === "Guest";
420
444
  }
421
445
 
422
446
  function orderFields(fields: DocTypeField[], fieldOrder: string[]): DocTypeField[] {
@@ -442,8 +466,8 @@ function orderFields(fields: DocTypeField[], fieldOrder: string[]): DocTypeField
442
466
  return ordered;
443
467
  }
444
468
 
445
- function parseRawDocTypeMeta(raw: RawDocTypeResponse): DocTypeMeta {
446
- const fields = raw.fields.map(parseRawField);
469
+ function parseRawDocTypeMeta(raw: DocTypeMeta): DocTypeMeta {
470
+ const fields = raw.fields.map((field) => ({ ...field }));
447
471
 
448
472
  // field_order may be a JSON string or an already-parsed array
449
473
  let fieldOrder: string[] | undefined;
@@ -458,20 +482,14 @@ function parseRawDocTypeMeta(raw: RawDocTypeResponse): DocTypeMeta {
458
482
  }
459
483
 
460
484
  const orderedFields = fieldOrder ? orderFields(fields, fieldOrder) : fields;
461
- const permissions = (raw.permissions ?? []).map(parseRawPermission);
462
- const sections = parseFormLayout(orderedFields);
463
485
 
464
486
  return {
465
- name: raw.name,
466
- module: raw.module,
467
- title_field: raw.title_field,
468
- image_field: raw.image_field,
469
- is_tree: !!raw.is_tree,
487
+ ...raw,
470
488
  sort_field: raw.sort_field || "modified",
471
489
  sort_order: raw.sort_order || "DESC",
472
490
  fields: orderedFields,
473
- sections,
474
- permissions,
491
+ permissions: raw.permissions ?? [],
492
+ sections: parseFormLayout(orderedFields),
475
493
  };
476
494
  }
477
495
 
@@ -533,6 +551,10 @@ function useHttpClientRef(
533
551
  * where the RPC is not registered. When `config.fixtures` carries a "DocType"
534
552
  * entry for the doctype it is used instead and no request is made.
535
553
  *
554
+ * Neither route is open to Guest. A signed-out connection (its token source
555
+ * resolves empty and no desk session cookie names a user) gets a 403
556
+ * `FrappeError` without a request, so callers keep their Guest fallbacks.
557
+ *
536
558
  * Responses are cached per-session in memory keyed by `{baseURL}:{doctype}`.
537
559
  * The `refetch` function clears the cache entry and forces a fresh fetch.
538
560
  *
@@ -545,7 +567,7 @@ function useHttpClientRef(
545
567
  * function PokemonForm() {
546
568
  * const { meta, isLoading } = useDocTypeMeta(config, "Pokemon");
547
569
  * if (isLoading) return <Spinner />;
548
- * return meta?.sections.map(section => (
570
+ * return meta?.sections?.map(section => (
549
571
  * <FormSection key={section.label} section={section} />
550
572
  * ));
551
573
  * }
@@ -613,7 +635,7 @@ export function useDocTypeMeta(
613
635
  // backoffice meta loader uses. Providers that stub only documents fall
614
636
  // through to the network below.
615
637
  const fromFixtures = config.fixtures
616
- ? ((await config.fixtures.getDoc("DocType", doctype)) as RawDocTypeResponse | null)
638
+ ? ((await config.fixtures.getDoc("DocType", doctype)) as DocTypeMeta | null)
617
639
  : null;
618
640
  if (cancelled) return;
619
641
 
@@ -628,13 +650,20 @@ export function useDocTypeMeta(
628
650
  );
629
651
  }
630
652
 
653
+ // Both routes below are closed to Guest, so a signed-out reader
654
+ // gets the 403 without the request (MPO-322).
655
+ if (await connectsAsGuest(config.auth)) {
656
+ if (cancelled) return;
657
+ throw new FrappeError(`Sign in to load the ${doctype} schema`, 403);
658
+ }
659
+
631
660
  // `getdoctype` is the whitelisted RPC the Desk itself loads form
632
661
  // metadata with, and every logged-in user may call it. Reading
633
662
  // `/api/resource/DocType/<name>` instead asks for the DocType
634
663
  // *document*, which only System Manager holds read permission on, so
635
664
  // ordinary users got a 403 PermissionError — and Frappe's
636
665
  // developer-mode traceback rendered into the UI instead of a form.
637
- const response = await http.get<{ docs?: RawDocTypeResponse[] }>(
666
+ const response = await http.get<{ docs?: DocTypeMeta[] }>(
638
667
  "/api/method/frappe.desk.form.load.getdoctype",
639
668
  { doctype },
640
669
  );
@@ -650,7 +679,7 @@ export function useDocTypeMeta(
650
679
  // above unregistered, so it answers with no `docs`. The document
651
680
  // route still serves meta there, and such a site's API clients are
652
681
  // typically System Managers, who may read it.
653
- const rest = await http.get<{ data?: RawDocTypeResponse }>(
682
+ const rest = await http.get<{ data?: DocTypeMeta }>(
654
683
  `/api/resource/DocType/${encodeURIComponent(doctype)}`,
655
684
  );
656
685
 
package/src/fixtures.ts CHANGED
@@ -234,6 +234,7 @@ export class InMemoryFixtureProvider implements FixtureProvider {
234
234
  doc: TDoc,
235
235
  fields: readonly string[],
236
236
  ): Partial<TDoc> {
237
+ if (fields.includes("*")) return { ...doc };
237
238
  const projected: any = {};
238
239
  for (const field of fields) {
239
240
  if (field in doc) {
@@ -0,0 +1,105 @@
1
+ import { act, renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, expect, it, vi } from "vitest";
3
+ import { getFrappeSyncModule, useFrappeCatchUp, useFrappePendingMutations } from "./frappeReact";
4
+ import { Operation, Status } from "./sync/types";
5
+ import { InMemoryFixtureProvider } from "./fixtures";
6
+ import type { SyncModule } from "./sync";
7
+ import type { UseFrappeCollectionConfig } from "./frappeReact";
8
+
9
+ const modules = new Set<SyncModule>();
10
+ function moduleFor(config: UseFrappeCollectionConfig) {
11
+ const sync = getFrappeSyncModule(config)!;
12
+ expect(sync).toBeDefined();
13
+ modules.add(sync);
14
+ return sync;
15
+ }
16
+ afterEach(async () => {
17
+ for (const sync of modules) await sync.dispose();
18
+ modules.clear();
19
+ vi.unstubAllGlobals();
20
+ });
21
+
22
+ it.each([false, true])(
23
+ "separates effective options in the shared cache, fixtures=%s",
24
+ (fixture) => {
25
+ const config = fixture
26
+ ? { fixtures: new InMemoryFixtureProvider({}) }
27
+ : { baseURL: "https://cache-options.test", realtime: false };
28
+ const defaultSync = moduleFor(config);
29
+ expect(moduleFor({ ...config, syncOptions: { catchUpStrategy: "cdc", cdcInterval: 0 } })).toBe(
30
+ defaultSync,
31
+ );
32
+ const snapshot = moduleFor({ ...config, syncOptions: { catchUpStrategy: "snapshot" } });
33
+ expect(snapshot).not.toBe(defaultSync);
34
+ expect(moduleFor({ ...config, syncOptions: { catchUpStrategy: "snapshot" } })).toBe(snapshot);
35
+ expect(moduleFor({ ...config, syncOptions: { cdcInterval: 1000 } })).not.toBe(defaultSync);
36
+ },
37
+ );
38
+
39
+ it("follows the selected module, retains failure state during retry and does not retry on render", async () => {
40
+ const transport = vi.fn(async () => Response.json({ data: [{ name: "a", doctype: "Task" }] }));
41
+ vi.stubGlobal("fetch", transport);
42
+ const cdc = { baseURL: "https://hook-options.test", realtime: false };
43
+ const snapshot = { ...cdc, syncOptions: { catchUpStrategy: "snapshot" as const } };
44
+ const first = moduleFor(cdc);
45
+ const second = moduleFor(snapshot);
46
+ await first.subscribe({ doctype: "Task" });
47
+ await second.subscribe({ doctype: "Task" });
48
+ const view = renderHook(({ config }) => useFrappeCatchUp(config), {
49
+ initialProps: { config: cdc as UseFrappeCollectionConfig | undefined },
50
+ });
51
+ transport.mockImplementation(async () =>
52
+ Response.json({ message: "unavailable" }, { status: 404 }),
53
+ );
54
+ await act(async () => {
55
+ await view.result.current.retry();
56
+ });
57
+ expect(view.result.current).toMatchObject({
58
+ refreshing: false,
59
+ stale: true,
60
+ error: { status: 404 },
61
+ });
62
+ const calls = transport.mock.calls.length;
63
+ view.rerender({ config: { ...cdc } });
64
+ expect(transport).toHaveBeenCalledTimes(calls);
65
+ view.rerender({ config: snapshot });
66
+ await waitFor(() => expect(view.result.current.stale).toBe(false));
67
+ transport.mockImplementation(async () => Response.json({ data: [] }));
68
+ await act(async () => {
69
+ await view.result.current.retry();
70
+ });
71
+ expect(view.result.current.error).toBeNull();
72
+ expect(second.__debug().subscriptions[0].docCount).toBe(0);
73
+ expect(first.__debug().subscriptions[0].docCount).toBe(1);
74
+ view.rerender({ config: undefined });
75
+ expect(await view.result.current.retry()).toEqual({
76
+ refreshing: false,
77
+ stale: false,
78
+ error: null,
79
+ });
80
+ view.unmount();
81
+ });
82
+
83
+ it("drops the previous module's mutation notice when changed options select an empty ledger", () => {
84
+ const config = { fixtures: new InMemoryFixtureProvider({}) };
85
+ const next = { ...config, syncOptions: { catchUpStrategy: "snapshot" as const } };
86
+ vi.spyOn(moduleFor(config), "getMutationEntries").mockReturnValue([
87
+ {
88
+ id: "old",
89
+ operation: Operation.Update,
90
+ doctype: "Task",
91
+ status: Status.Failed,
92
+ createdAt: 1,
93
+ retryCount: 0,
94
+ error: "old module failure",
95
+ },
96
+ ]);
97
+ moduleFor(next);
98
+ const view = renderHook(({ connection }) => useFrappePendingMutations(connection), {
99
+ initialProps: { connection: config as UseFrappeCollectionConfig },
100
+ });
101
+ expect(view.result.current.failed.map((entry) => entry.id)).toEqual(["old"]);
102
+ view.rerender({ connection: next });
103
+ expect(view.result.current.entries).toEqual([]);
104
+ view.unmount();
105
+ });