@multiplatform.one/frappe 7.11.0 → 7.16.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/README.md +1 -1
- package/dist/cjs/auth.native.js.map +1 -1
- package/dist/cjs/call.native.js.map +1 -1
- package/dist/cjs/collection.native.js.map +1 -1
- package/dist/cjs/common/types.native.js.map +1 -1
- package/dist/cjs/core/http.native.js.map +1 -1
- package/dist/cjs/core/types.native.js.map +1 -1
- package/dist/cjs/core/wire.native.js.map +1 -1
- package/dist/cjs/db.native.js.map +1 -1
- package/dist/cjs/devtools/FrappeDevtoolsPanel.native.js.map +1 -1
- package/dist/cjs/devtools/index.native.js.map +1 -1
- package/dist/cjs/devtools/plugin.native.js.map +1 -1
- package/dist/cjs/devtools/theme.native.js.map +1 -1
- package/dist/cjs/doctype.cjs +25 -43
- package/dist/cjs/doctype.native.js +27 -43
- package/dist/cjs/doctype.native.js.map +1 -1
- package/dist/cjs/file.native.js.map +1 -1
- package/dist/cjs/fixtureDb.native.js.map +1 -1
- package/dist/cjs/fixtures.cjs +3 -0
- package/dist/cjs/fixtures.native.js +3 -0
- package/dist/cjs/fixtures.native.js.map +1 -1
- package/dist/cjs/frappeApp.native.js.map +1 -1
- package/dist/cjs/frappeReact.cjs +13 -1
- package/dist/cjs/frappeReact.native.js +20 -2
- package/dist/cjs/frappeReact.native.js.map +1 -1
- package/dist/cjs/index.cjs +6 -0
- package/dist/cjs/index.native.js +8 -2
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/optimistic.native.js.map +1 -1
- package/dist/cjs/pacer.native.js.map +1 -1
- package/dist/cjs/plugin/index.native.js.map +1 -1
- package/dist/cjs/realtime.native.js.map +1 -1
- package/dist/cjs/serverLoaders.cjs +74 -0
- package/dist/cjs/serverLoaders.native.js +88 -0
- package/dist/cjs/serverLoaders.native.js.map +1 -0
- package/dist/cjs/sync/backfill.native.js.map +1 -1
- package/dist/cjs/sync/filters.native.js.map +1 -1
- package/dist/cjs/sync/index.cjs +8 -0
- package/dist/cjs/sync/index.native.js +10 -0
- package/dist/cjs/sync/index.native.js.map +1 -1
- package/dist/cjs/sync/merge.native.js.map +1 -1
- package/dist/cjs/sync/mutationEngine.native.js.map +1 -1
- package/dist/cjs/sync/mutationJournal.native.js.map +1 -1
- package/dist/cjs/sync/normalizedCache.native.js.map +1 -1
- package/dist/cjs/sync/persistence.native.js.map +1 -1
- package/dist/cjs/sync/realtime.native.js.map +1 -1
- package/dist/cjs/sync/rehydration.cjs +4 -0
- package/dist/cjs/sync/rehydration.native.js +6 -0
- package/dist/cjs/sync/rehydration.native.js.map +1 -1
- package/dist/cjs/sync/storage.native.js.map +1 -1
- package/dist/cjs/sync/subscriptionManager.native.js.map +1 -1
- package/dist/cjs/sync/types.native.js.map +1 -1
- package/dist/cjs/sync/virtualPageManager.native.js.map +1 -1
- package/dist/cjs/syncOptionsKey.native.js.map +1 -1
- package/dist/cjs/useLiveQuery.cjs +2 -20
- package/dist/cjs/useLiveQuery.native.js +1 -22
- package/dist/cjs/useLiveQuery.native.js.map +1 -1
- package/dist/cjs/util.native.js.map +1 -1
- package/dist/esm/doctype.mjs +23 -44
- package/dist/esm/doctype.mjs.map +1 -1
- package/dist/esm/doctype.native.js +25 -44
- package/dist/esm/doctype.native.js.map +1 -1
- package/dist/esm/fixtures.mjs +3 -0
- package/dist/esm/fixtures.mjs.map +1 -1
- package/dist/esm/fixtures.native.js +3 -0
- package/dist/esm/fixtures.native.js.map +1 -1
- package/dist/esm/frappeReact.mjs +13 -1
- package/dist/esm/frappeReact.mjs.map +1 -1
- package/dist/esm/frappeReact.native.js +19 -1
- package/dist/esm/frappeReact.native.js.map +1 -1
- package/dist/esm/index.mjs +3 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +3 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/serverLoaders.mjs +48 -0
- package/dist/esm/serverLoaders.mjs.map +1 -0
- package/dist/esm/serverLoaders.native.js +59 -0
- package/dist/esm/serverLoaders.native.js.map +1 -0
- package/dist/esm/sync/index.mjs +8 -0
- package/dist/esm/sync/index.mjs.map +1 -1
- package/dist/esm/sync/index.native.js +10 -0
- package/dist/esm/sync/index.native.js.map +1 -1
- package/dist/esm/sync/rehydration.mjs +4 -0
- package/dist/esm/sync/rehydration.mjs.map +1 -1
- package/dist/esm/sync/rehydration.native.js +6 -0
- package/dist/esm/sync/rehydration.native.js.map +1 -1
- package/dist/esm/useLiveQuery.mjs +1 -19
- package/dist/esm/useLiveQuery.mjs.map +1 -1
- package/dist/esm/useLiveQuery.native.js +1 -22
- package/dist/esm/useLiveQuery.native.js.map +1 -1
- package/dist/jsx/doctype.mjs +23 -44
- package/dist/jsx/doctype.mjs.map +1 -1
- package/dist/jsx/doctype.native.js +27 -43
- package/dist/jsx/doctype.native.js.map +1 -1
- package/dist/jsx/fixtures.mjs +3 -0
- package/dist/jsx/fixtures.mjs.map +1 -1
- package/dist/jsx/fixtures.native.js +3 -0
- package/dist/jsx/fixtures.native.js.map +1 -1
- package/dist/jsx/frappeReact.mjs +13 -1
- package/dist/jsx/frappeReact.mjs.map +1 -1
- package/dist/jsx/frappeReact.native.js +19 -1
- package/dist/jsx/frappeReact.native.js.map +1 -1
- package/dist/jsx/index.js +3 -2
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +3 -2
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +6 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/serverLoaders.mjs +48 -0
- package/dist/jsx/serverLoaders.mjs.map +1 -0
- package/dist/jsx/serverLoaders.native.js +88 -0
- package/dist/jsx/serverLoaders.native.js.map +1 -0
- package/dist/jsx/sync/index.mjs +8 -0
- package/dist/jsx/sync/index.mjs.map +1 -1
- package/dist/jsx/sync/index.native.js +10 -0
- package/dist/jsx/sync/index.native.js.map +1 -1
- package/dist/jsx/sync/rehydration.mjs +4 -0
- package/dist/jsx/sync/rehydration.mjs.map +1 -1
- package/dist/jsx/sync/rehydration.native.js +6 -0
- package/dist/jsx/sync/rehydration.native.js.map +1 -1
- package/dist/jsx/useLiveQuery.mjs +1 -19
- package/dist/jsx/useLiveQuery.mjs.map +1 -1
- package/dist/jsx/useLiveQuery.native.js +1 -22
- package/dist/jsx/useLiveQuery.native.js.map +1 -1
- package/package.json +9 -9
- package/src/doctype.spec.ts +148 -5
- package/src/doctype.ts +202 -173
- package/src/fixtures.ts +1 -0
- package/src/frappeReact.hydration.spec.tsx +164 -0
- package/src/frappeReact.spec.ts +56 -0
- package/src/frappeReact.ts +31 -5
- package/src/index.ts +11 -0
- package/src/serverLoaders.ts +77 -0
- package/src/sync/index.ts +9 -0
- package/src/sync/rehydration.ts +5 -0
- package/src/sync/syncModule.spec.ts +20 -0
- package/src/useLiveQuery.hydration.spec.tsx +129 -0
- package/src/useLiveQuery.spec.ts +3 -11
- package/src/useLiveQuery.ts +3 -57
- package/types/doctype.d.ts +117 -33
- package/types/doctype.d.ts.map +1 -1
- package/types/fixtures.d.ts.map +1 -1
- package/types/frappeReact.d.ts +3 -2
- package/types/frappeReact.d.ts.map +1 -1
- package/types/index.d.ts +3 -2
- package/types/index.d.ts.map +1 -1
- package/types/serverLoaders.d.ts +40 -0
- package/types/serverLoaders.d.ts.map +1 -0
- package/types/sync/index.d.ts +6 -0
- package/types/sync/index.d.ts.map +1 -1
- package/types/sync/rehydration.d.ts +2 -0
- package/types/sync/rehydration.d.ts.map +1 -1
- package/types/useLiveQuery.d.ts +3 -13
- 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
|
|
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
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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?:
|
|
70
|
-
unique?:
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
129
|
+
idx?: number;
|
|
130
|
+
[key: string]: unknown;
|
|
85
131
|
}
|
|
86
132
|
|
|
87
|
-
/**
|
|
133
|
+
/** Role permission row from the DocType meta. */
|
|
88
134
|
export interface DocTypePermission {
|
|
89
|
-
role
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
120
|
-
sort_field
|
|
121
|
-
sort_order
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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:
|
|
446
|
-
const fields = raw.fields.map(
|
|
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
|
-
|
|
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
|
-
|
|
474
|
-
|
|
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
|
|
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
|
|
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?:
|
|
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?:
|
|
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,164 @@
|
|
|
1
|
+
import { act, Suspense } from "react";
|
|
2
|
+
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { renderToString } from "react-dom/server";
|
|
4
|
+
import { expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const runtime = vi.hoisted(() => ({ client: false }));
|
|
7
|
+
const { mockSubscribe, mockUnsubscribe, mockGetDocsForSubscription, storeListeners, docStore } =
|
|
8
|
+
vi.hoisted(() => {
|
|
9
|
+
const listeners = new Set<() => void>();
|
|
10
|
+
const docs = new Map<string, Array<{ name: string; doctype: string; title: string }>>();
|
|
11
|
+
let subscriptionCounter = 0;
|
|
12
|
+
return {
|
|
13
|
+
storeListeners: listeners,
|
|
14
|
+
docStore: docs,
|
|
15
|
+
mockSubscribe: vi.fn(async (query: { doctype: string }) => {
|
|
16
|
+
const subscriptionId = `sub-${++subscriptionCounter}`;
|
|
17
|
+
docs.set(subscriptionId, [
|
|
18
|
+
{ name: "ws-1", doctype: query.doctype, title: "Workspace One" },
|
|
19
|
+
{ name: "ws-2", doctype: query.doctype, title: "Workspace Two" },
|
|
20
|
+
]);
|
|
21
|
+
return { subscriptionId, rows: docs.get(subscriptionId), checkpoint: "t0" };
|
|
22
|
+
}),
|
|
23
|
+
mockUnsubscribe: vi.fn((subscriptionId: string) => {
|
|
24
|
+
docs.delete(subscriptionId);
|
|
25
|
+
}),
|
|
26
|
+
mockGetDocsForSubscription: vi.fn((subscriptionId: string) => {
|
|
27
|
+
return docs.get(subscriptionId) ?? [];
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
vi.mock("./sync", () => ({
|
|
33
|
+
SyncModule: class {
|
|
34
|
+
subscribe = mockSubscribe;
|
|
35
|
+
unsubscribe = mockUnsubscribe;
|
|
36
|
+
getDocsForSubscription = mockGetDocsForSubscription;
|
|
37
|
+
subscribeStore = (listener: () => void) => {
|
|
38
|
+
storeListeners.add(listener);
|
|
39
|
+
return () => storeListeners.delete(listener);
|
|
40
|
+
};
|
|
41
|
+
mutate = vi.fn();
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
vi.mock("./realtime", () => ({ RealtimeModule: class {} }));
|
|
45
|
+
vi.mock("./core/http", () => ({ HttpClient: class {} }));
|
|
46
|
+
vi.mock("./db", () => ({ DbModule: class {} }));
|
|
47
|
+
vi.mock("./util", () => ({ isBrowser: () => runtime.client }));
|
|
48
|
+
vi.mock("./fixtureDb", () => ({ FixtureDbModule: class {} }));
|
|
49
|
+
|
|
50
|
+
import { useFrappeCollection } from "./frappeReact";
|
|
51
|
+
import { useLiveQuery } from "./useLiveQuery";
|
|
52
|
+
|
|
53
|
+
type Doc = { name: string; doctype: string; title: string };
|
|
54
|
+
|
|
55
|
+
it.each([false, true])(
|
|
56
|
+
"hydrates a cached collection after an earlier reader (delayed=%s)",
|
|
57
|
+
async (delayed) => {
|
|
58
|
+
const config = { baseURL: `https://neutral-${delayed}.invalid`, realtime: false };
|
|
59
|
+
const collections = new Set<NonNullable<ReturnType<typeof useFrappeCollection<Doc>>>>();
|
|
60
|
+
let release: () => void = () => {};
|
|
61
|
+
let wait = false;
|
|
62
|
+
const pending = new Promise<void>((resolve) => {
|
|
63
|
+
release = resolve;
|
|
64
|
+
});
|
|
65
|
+
function Reader({ late = false, enabled = true }: { late?: boolean; enabled?: boolean }) {
|
|
66
|
+
if (late && wait) throw pending;
|
|
67
|
+
const collection = useFrappeCollection<Doc>(enabled ? config : undefined, "Workspace");
|
|
68
|
+
if (collection) collections.add(collection);
|
|
69
|
+
const result = useLiveQuery(
|
|
70
|
+
(q) => (collection ? q.from({ item: collection }) : null),
|
|
71
|
+
[collection],
|
|
72
|
+
);
|
|
73
|
+
return (
|
|
74
|
+
<div data-result={late ? "late" : "early"}>
|
|
75
|
+
{!enabled
|
|
76
|
+
? "Disabled"
|
|
77
|
+
: !collection || result.isLoading
|
|
78
|
+
? "Loading"
|
|
79
|
+
: result.data?.map((row) => row.title).join(",")}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const view = (
|
|
84
|
+
<Suspense fallback={<span>Boundary pending</span>}>
|
|
85
|
+
<Reader late />
|
|
86
|
+
</Suspense>
|
|
87
|
+
);
|
|
88
|
+
runtime.client = false;
|
|
89
|
+
const host = document.createElement("div");
|
|
90
|
+
host.innerHTML = renderToString(view);
|
|
91
|
+
expect(host.textContent).toBe("Loading");
|
|
92
|
+
const serverNode = host.querySelector("[data-result]");
|
|
93
|
+
document.body.append(host);
|
|
94
|
+
const earlyHost = document.createElement("div");
|
|
95
|
+
document.body.append(earlyHost);
|
|
96
|
+
runtime.client = true;
|
|
97
|
+
const errors = vi.fn();
|
|
98
|
+
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
99
|
+
let early: Root | undefined;
|
|
100
|
+
let late: Root | undefined;
|
|
101
|
+
try {
|
|
102
|
+
await act(async () => {
|
|
103
|
+
early = createRoot(earlyHost);
|
|
104
|
+
early.render(<Reader />);
|
|
105
|
+
});
|
|
106
|
+
await vi.waitFor(() => expect(earlyHost.textContent).toContain("Workspace One"));
|
|
107
|
+
const warm = [...collections][0];
|
|
108
|
+
expect(warm.status).toBe("ready");
|
|
109
|
+
wait = delayed;
|
|
110
|
+
await act(async () => {
|
|
111
|
+
late = hydrateRoot(host, view, { onRecoverableError: errors });
|
|
112
|
+
});
|
|
113
|
+
if (delayed) {
|
|
114
|
+
expect(host.querySelector("[data-result]")).toBe(serverNode);
|
|
115
|
+
expect(host.textContent).toBe("Loading");
|
|
116
|
+
await act(async () => {
|
|
117
|
+
wait = false;
|
|
118
|
+
release();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
await vi.waitFor(() => expect(host.textContent).toContain("Workspace One"));
|
|
122
|
+
expect(errors.mock.calls.map(([e]) => String(e))).toEqual([]);
|
|
123
|
+
expect(
|
|
124
|
+
consoleError.mock.calls.filter((args) => /hydration|did not match/i.test(args.join(" "))),
|
|
125
|
+
).toEqual([]);
|
|
126
|
+
expect(host.querySelector("[data-result]")).toBe(serverNode);
|
|
127
|
+
expect(collections.size).toBe(1);
|
|
128
|
+
await act(async () => {
|
|
129
|
+
for (const docs of docStore.values())
|
|
130
|
+
docs.push({ name: "ws-3", doctype: "Workspace", title: "Workspace Three" });
|
|
131
|
+
for (const listener of storeListeners) listener();
|
|
132
|
+
});
|
|
133
|
+
await vi.waitFor(() => expect(host.textContent).toContain("Workspace Three"));
|
|
134
|
+
await act(async () => late!.render(<Reader enabled={false} />));
|
|
135
|
+
expect(host.textContent).toBe("Disabled");
|
|
136
|
+
// A fresh boundary must use its own SSR snapshot after the shared cache
|
|
137
|
+
// and an earlier boundary have both committed updates.
|
|
138
|
+
await act(async () => late!.unmount());
|
|
139
|
+
runtime.client = false;
|
|
140
|
+
host.innerHTML = renderToString(view);
|
|
141
|
+
expect(host.textContent).toBe("Loading");
|
|
142
|
+
const freshServerNode = host.querySelector("[data-result]");
|
|
143
|
+
runtime.client = true;
|
|
144
|
+
await act(async () => {
|
|
145
|
+
late = hydrateRoot(host, view, { onRecoverableError: errors });
|
|
146
|
+
});
|
|
147
|
+
await vi.waitFor(() => expect(host.textContent).toContain("Workspace Three"));
|
|
148
|
+
expect(host.querySelector("[data-result]")).toBe(freshServerNode);
|
|
149
|
+
expect(errors).not.toHaveBeenCalled();
|
|
150
|
+
} finally {
|
|
151
|
+
await act(async () => {
|
|
152
|
+
early?.unmount();
|
|
153
|
+
late?.unmount();
|
|
154
|
+
});
|
|
155
|
+
host.remove();
|
|
156
|
+
earlyHost.remove();
|
|
157
|
+
consoleError.mockRestore();
|
|
158
|
+
}
|
|
159
|
+
await vi.waitFor(
|
|
160
|
+
() => expect([...collections].every((c) => c.subscriberCount === 0)).toBe(true),
|
|
161
|
+
{ timeout: 10000 },
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
);
|