grist-widget-sdk 0.3.26 → 0.4.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/CHANGELOG.md +53 -0
- package/README.md +1 -1
- package/dist/app.cjs +1139 -4336
- package/dist/app.cjs.map +1 -1
- package/dist/app.d.cts +1 -3
- package/dist/app.d.ts +1 -3
- package/dist/app.js +9 -517
- package/dist/app.js.map +1 -1
- package/dist/chunk-4ZGVKGDF.js +204 -0
- package/dist/chunk-4ZGVKGDF.js.map +1 -0
- package/dist/{chunk-YQNMIL33.js → chunk-B5ZZNLVS.js} +528 -181
- package/dist/chunk-B5ZZNLVS.js.map +1 -0
- package/dist/chunk-O7NY46QY.js +1830 -0
- package/dist/chunk-O7NY46QY.js.map +1 -0
- package/dist/chunk-P6VTEKWO.js +618 -0
- package/dist/chunk-P6VTEKWO.js.map +1 -0
- package/dist/emulator/testing.cjs +2084 -391
- package/dist/emulator/testing.cjs.map +1 -1
- package/dist/emulator/testing.d.cts +55 -7
- package/dist/emulator/testing.d.ts +55 -7
- package/dist/emulator/testing.js +144 -19
- package/dist/emulator/testing.js.map +1 -1
- package/dist/emulator.cjs +59 -249
- package/dist/emulator.cjs.map +1 -1
- package/dist/emulator.d.cts +3 -5
- package/dist/emulator.d.ts +3 -5
- package/dist/emulator.js +5 -11
- package/dist/index.cjs +1373 -4859
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -260
- package/dist/index.d.ts +81 -260
- package/dist/index.js +8 -161
- package/dist/{inline-C7DkvuET.d.cts → templates-CGmRIQBV.d.cts} +89 -35
- package/dist/{inline-DpcT8tV9.d.ts → templates-Cq94qOcf.d.ts} +89 -35
- package/dist/{types-D3JxUCfL.d.cts → types-DBStn0Dk.d.cts} +2 -26
- package/dist/{types-D3JxUCfL.d.ts → types-DBStn0Dk.d.ts} +2 -26
- package/dist/use-grist-BK0eke9K.d.cts +358 -0
- package/dist/use-grist-C5DMZ3wU.d.ts +358 -0
- package/package.json +8 -37
- package/dist/advanced.cjs +0 -2799
- package/dist/advanced.cjs.map +0 -1
- package/dist/advanced.d.cts +0 -822
- package/dist/advanced.d.ts +0 -822
- package/dist/advanced.js +0 -875
- package/dist/advanced.js.map +0 -1
- package/dist/chunk-22U7WAY3.js +0 -463
- package/dist/chunk-22U7WAY3.js.map +0 -1
- package/dist/chunk-64SVRWW7.js +0 -758
- package/dist/chunk-64SVRWW7.js.map +0 -1
- package/dist/chunk-ELGVOHFD.js +0 -32
- package/dist/chunk-ELGVOHFD.js.map +0 -1
- package/dist/chunk-EPEBKXVN.js +0 -470
- package/dist/chunk-EPEBKXVN.js.map +0 -1
- package/dist/chunk-GTTD5HPO.js +0 -36
- package/dist/chunk-GTTD5HPO.js.map +0 -1
- package/dist/chunk-JASMM2YC.js +0 -243
- package/dist/chunk-JASMM2YC.js.map +0 -1
- package/dist/chunk-N3X46JW2.js +0 -37
- package/dist/chunk-N3X46JW2.js.map +0 -1
- package/dist/chunk-PDGEZ24B.js +0 -2366
- package/dist/chunk-PDGEZ24B.js.map +0 -1
- package/dist/chunk-PM3B4WYV.js +0 -12
- package/dist/chunk-PM3B4WYV.js.map +0 -1
- package/dist/chunk-REV5AUXD.js +0 -25
- package/dist/chunk-REV5AUXD.js.map +0 -1
- package/dist/chunk-TZB3OZA6.js +0 -1630
- package/dist/chunk-TZB3OZA6.js.map +0 -1
- package/dist/chunk-YQNMIL33.js.map +0 -1
- package/dist/create-grist-emulator-BCqp1BF9.d.ts +0 -81
- package/dist/create-grist-emulator-DBM8OSIR.d.cts +0 -81
- package/dist/emulator/devtools.cjs +0 -1404
- package/dist/emulator/devtools.cjs.map +0 -1
- package/dist/emulator/devtools.d.cts +0 -48
- package/dist/emulator/devtools.d.ts +0 -48
- package/dist/emulator/devtools.js +0 -508
- package/dist/emulator/devtools.js.map +0 -1
- package/dist/emulator/transports.cjs +0 -1132
- package/dist/emulator/transports.cjs.map +0 -1
- package/dist/emulator/transports.d.cts +0 -38
- package/dist/emulator/transports.d.ts +0 -38
- package/dist/emulator/transports.js +0 -12
- package/dist/emulator/transports.js.map +0 -1
- package/dist/templates-8k2LKoO-.d.cts +0 -24
- package/dist/templates-DIwi_mvI.d.ts +0 -24
- package/dist/use-grist-reads-DUMvJgV7.d.ts +0 -480
- package/dist/use-grist-reads-DpjVuI0H.d.cts +0 -480
- package/dist/use-grist-types-DdOLEJXN.d.cts +0 -499
- package/dist/use-grist-types-kwxoFeub.d.ts +0 -499
- package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.cts +0 -43
- package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.ts +0 -43
|
@@ -0,0 +1,1830 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildReplicaJsonPayload,
|
|
3
|
+
coerceGristDocApiRefRowId,
|
|
4
|
+
decodeGristValue,
|
|
5
|
+
encodeGristValue
|
|
6
|
+
} from "./chunk-4ZGVKGDF.js";
|
|
7
|
+
|
|
8
|
+
// src/sdk/helpers/grist-table-data.ts
|
|
9
|
+
function computeRowCount(data) {
|
|
10
|
+
const idCol = data.id;
|
|
11
|
+
if (Array.isArray(idCol)) return idCol.length;
|
|
12
|
+
for (const key of Object.keys(data)) {
|
|
13
|
+
const col = data[key];
|
|
14
|
+
if (Array.isArray(col)) return col.length;
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
function decodeColumnarToReplicaRows(data, columnIds, columns, maxRows) {
|
|
19
|
+
const nFull = computeRowCount(data);
|
|
20
|
+
const n = maxRows !== void 0 ? Math.min(nFull, maxRows) : nFull;
|
|
21
|
+
const out = [];
|
|
22
|
+
for (let i = 0; i < n; i++) {
|
|
23
|
+
const row = {};
|
|
24
|
+
for (const k of columnIds) {
|
|
25
|
+
const arr = data[k];
|
|
26
|
+
const raw = Array.isArray(arr) ? arr[i] : void 0;
|
|
27
|
+
if (k === "id") {
|
|
28
|
+
row[k] = raw;
|
|
29
|
+
} else {
|
|
30
|
+
row[k] = decodeGristValue(raw, columns[k]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
out.push(row);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function tableDataToRows(data) {
|
|
38
|
+
const idCol = data.id;
|
|
39
|
+
if (!Array.isArray(idCol) || idCol.length === 0) return [];
|
|
40
|
+
const keys = Object.keys(data);
|
|
41
|
+
const n = idCol.length;
|
|
42
|
+
const rows = [];
|
|
43
|
+
for (let i = 0; i < n; i++) {
|
|
44
|
+
const row = {};
|
|
45
|
+
for (const k of keys) {
|
|
46
|
+
const col = data[k];
|
|
47
|
+
row[k] = Array.isArray(col) ? col[i] : void 0;
|
|
48
|
+
}
|
|
49
|
+
rows.push(row);
|
|
50
|
+
}
|
|
51
|
+
return rows;
|
|
52
|
+
}
|
|
53
|
+
function manualSortKey(row) {
|
|
54
|
+
const v = row.manualSort;
|
|
55
|
+
if (typeof v === "number" && !Number.isNaN(v)) return v;
|
|
56
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
57
|
+
const n = Number(v);
|
|
58
|
+
if (!Number.isNaN(n)) return n;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function sortRowsByGristOrder(rows) {
|
|
63
|
+
const hasManual = rows.some((r) => manualSortKey(r) !== null);
|
|
64
|
+
if (!hasManual) {
|
|
65
|
+
return [...rows].sort((a, b) => {
|
|
66
|
+
const ida = typeof a.id === "number" ? a.id : 0;
|
|
67
|
+
const idb = typeof b.id === "number" ? b.id : 0;
|
|
68
|
+
return ida - idb;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return [...rows].sort((a, b) => {
|
|
72
|
+
const ma = manualSortKey(a);
|
|
73
|
+
const mb = manualSortKey(b);
|
|
74
|
+
if (ma !== null && mb !== null && ma !== mb) return ma - mb;
|
|
75
|
+
if (ma !== null && mb === null) return -1;
|
|
76
|
+
if (ma === null && mb !== null) return 1;
|
|
77
|
+
const ida = typeof a.id === "number" ? a.id : 0;
|
|
78
|
+
const idb = typeof b.id === "number" ? b.id : 0;
|
|
79
|
+
return ida - idb;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function materializeGristRowsFromColumnar(data, options) {
|
|
83
|
+
const order = options?.order ?? "grist";
|
|
84
|
+
let rows;
|
|
85
|
+
if (options?.columns) {
|
|
86
|
+
const columnIds = Object.keys(data);
|
|
87
|
+
rows = decodeColumnarToReplicaRows(data, columnIds, options.columns);
|
|
88
|
+
} else {
|
|
89
|
+
rows = tableDataToRows(data);
|
|
90
|
+
}
|
|
91
|
+
if (order === "none") return rows;
|
|
92
|
+
if (order === "id") {
|
|
93
|
+
return [...rows].sort((a, b) => {
|
|
94
|
+
const ida = typeof a.id === "number" ? a.id : 0;
|
|
95
|
+
const idb = typeof b.id === "number" ? b.id : 0;
|
|
96
|
+
return ida - idb;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return sortRowsByGristOrder(rows);
|
|
100
|
+
}
|
|
101
|
+
async function fetchGristTableRows(docApi, tableId, options) {
|
|
102
|
+
const data = await docApi.fetchTable(tableId);
|
|
103
|
+
return materializeGristRowsFromColumnar(data, options);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/sdk/helpers/grist-schema-formula.ts
|
|
107
|
+
function inferGristFormulaKind(isFormula, formulaTrimmed) {
|
|
108
|
+
if (formulaTrimmed.length > 0) return isFormula ? "computed" : "trigger";
|
|
109
|
+
return isFormula ? "empty_slot" : "none";
|
|
110
|
+
}
|
|
111
|
+
function resolveGristFormulaKind(col) {
|
|
112
|
+
if (col.formulaKind) return col.formulaKind;
|
|
113
|
+
const f = col.formula?.trim() ?? "";
|
|
114
|
+
if (f.length > 0) return col.isFormula === true ? "computed" : "trigger";
|
|
115
|
+
return col.isFormula === true ? "empty_slot" : "none";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/sdk/helpers/grist-widget-options.ts
|
|
119
|
+
function parseGristWidgetOptionsCell(raw) {
|
|
120
|
+
if (raw == null || raw === "") return void 0;
|
|
121
|
+
if (typeof raw === "string") {
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(raw);
|
|
124
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
if (typeof raw === "object" && !Array.isArray(raw)) {
|
|
133
|
+
return raw;
|
|
134
|
+
}
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
function readStyleColor(style, key) {
|
|
138
|
+
const v = style[key];
|
|
139
|
+
return typeof v === "string" ? v : void 0;
|
|
140
|
+
}
|
|
141
|
+
function normalizeGristChoiceListEntries(widgetOptions) {
|
|
142
|
+
if (!widgetOptions) return [];
|
|
143
|
+
const rawChoices = widgetOptions.choices;
|
|
144
|
+
if (!Array.isArray(rawChoices)) return [];
|
|
145
|
+
const choiceOptionsRaw = widgetOptions.choiceOptions;
|
|
146
|
+
const choiceOptions = choiceOptionsRaw && typeof choiceOptionsRaw === "object" && !Array.isArray(choiceOptionsRaw) ? choiceOptionsRaw : {};
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const c of rawChoices) {
|
|
149
|
+
const value = typeof c === "string" ? c : c != null ? String(c) : "";
|
|
150
|
+
if (!value) continue;
|
|
151
|
+
const style = choiceOptions[value] ?? {};
|
|
152
|
+
out.push({
|
|
153
|
+
value,
|
|
154
|
+
label: value,
|
|
155
|
+
fillColor: readStyleColor(style, "fillColor"),
|
|
156
|
+
textColor: readStyleColor(style, "textColor")
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/sdk/helpers/grist-attachment-download.ts
|
|
163
|
+
function attachmentIdFromObjectRecord(record) {
|
|
164
|
+
for (const candidate of [record.id, record.attachmentId, record.fileId]) {
|
|
165
|
+
if (candidate === void 0 || candidate === null) continue;
|
|
166
|
+
return String(candidate);
|
|
167
|
+
}
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
function extractGristAttachmentIdsFromCell(value) {
|
|
171
|
+
const out = [];
|
|
172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
173
|
+
const push = (id) => {
|
|
174
|
+
if (seen.has(id)) return;
|
|
175
|
+
seen.add(id);
|
|
176
|
+
out.push(id);
|
|
177
|
+
};
|
|
178
|
+
const walk = (v) => {
|
|
179
|
+
if (v === void 0 || v === null) return;
|
|
180
|
+
if (typeof v === "number") {
|
|
181
|
+
push(String(v));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (typeof v === "string") {
|
|
185
|
+
const trimmed = v.trim();
|
|
186
|
+
if (!trimmed) return;
|
|
187
|
+
if (/^\d+$/.test(trimmed)) {
|
|
188
|
+
push(trimmed);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
walk(JSON.parse(trimmed));
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(v)) {
|
|
198
|
+
for (const item of v) walk(item);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (typeof v === "object") {
|
|
202
|
+
const record = v;
|
|
203
|
+
const direct = attachmentIdFromObjectRecord(record);
|
|
204
|
+
if (direct !== void 0) {
|
|
205
|
+
push(direct);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (const nested of Object.values(record)) walk(nested);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
walk(value);
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
function extractGristAttachmentId(value) {
|
|
215
|
+
return extractGristAttachmentIdsFromCell(value)[0];
|
|
216
|
+
}
|
|
217
|
+
async function getAttachmentDownloadUrl(docApi, attachmentId, options) {
|
|
218
|
+
const getAccessToken = docApi.getAccessToken;
|
|
219
|
+
if (!getAccessToken) {
|
|
220
|
+
throw new Error("getAccessToken is not available on docApi");
|
|
221
|
+
}
|
|
222
|
+
const { token, baseUrl } = await getAccessToken({ readOnly: options?.readOnly ?? false });
|
|
223
|
+
return `${baseUrl}/attachments/${attachmentId}/download?auth=${token}`;
|
|
224
|
+
}
|
|
225
|
+
async function fetchBytesFromSignedAttachmentUrl(url) {
|
|
226
|
+
const response = await fetch(url);
|
|
227
|
+
const contentType = response.headers.get("content-type")?.trim() || "application/octet-stream";
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
throw new Error(`Attachment download failed (${response.status})`);
|
|
230
|
+
}
|
|
231
|
+
const buffer = await response.arrayBuffer();
|
|
232
|
+
return { buffer, contentType };
|
|
233
|
+
}
|
|
234
|
+
async function fetchAttachmentBlob(docApi, attachmentId, options) {
|
|
235
|
+
const url = await getAttachmentDownloadUrl(docApi, attachmentId, options);
|
|
236
|
+
const { buffer, contentType } = await fetchBytesFromSignedAttachmentUrl(url);
|
|
237
|
+
return { blob: new Blob([buffer], { type: contentType }), contentType };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/sdk/helpers/grist-fetch-with-auth.ts
|
|
241
|
+
var TOKEN_TTL_MS = 45e3;
|
|
242
|
+
var cache = /* @__PURE__ */ new Map();
|
|
243
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
244
|
+
function cacheKey(readOnly) {
|
|
245
|
+
return readOnly ? "ro" : "rw";
|
|
246
|
+
}
|
|
247
|
+
async function getGristAccessToken(docApi, options) {
|
|
248
|
+
if (!docApi.getAccessToken) {
|
|
249
|
+
throw new Error("getAccessToken is not available on docApi (need higher access level)");
|
|
250
|
+
}
|
|
251
|
+
const readOnly = options?.readOnly ?? false;
|
|
252
|
+
const key = cacheKey(readOnly);
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
const hit = cache.get(key);
|
|
255
|
+
if (hit && now - hit.fetchedAt < TOKEN_TTL_MS) {
|
|
256
|
+
return hit.token;
|
|
257
|
+
}
|
|
258
|
+
const pending = inFlight.get(key);
|
|
259
|
+
if (pending) return pending;
|
|
260
|
+
const next = docApi.getAccessToken({ readOnly }).then((token) => {
|
|
261
|
+
cache.set(key, { token, fetchedAt: Date.now() });
|
|
262
|
+
inFlight.delete(key);
|
|
263
|
+
return token;
|
|
264
|
+
});
|
|
265
|
+
inFlight.set(key, next);
|
|
266
|
+
return next;
|
|
267
|
+
}
|
|
268
|
+
function clearGristAccessTokenCache() {
|
|
269
|
+
cache.clear();
|
|
270
|
+
inFlight.clear();
|
|
271
|
+
}
|
|
272
|
+
async function fetchWithGristAuth(docApi, path, init) {
|
|
273
|
+
const readOnly = init?.readOnly ?? false;
|
|
274
|
+
let token;
|
|
275
|
+
try {
|
|
276
|
+
token = await getGristAccessToken(docApi, { readOnly });
|
|
277
|
+
} catch (err) {
|
|
278
|
+
clearGristAccessTokenCache();
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
const isAbsolute = /^https?:\/\//i.test(path);
|
|
282
|
+
const cleanPath = isAbsolute ? path : path.startsWith("/") ? path : `/${path}`;
|
|
283
|
+
const url = isAbsolute ? new URL(path) : new URL(`${token.baseUrl.replace(/\/$/, "")}${cleanPath}`);
|
|
284
|
+
const gristOrigin = new URL(token.baseUrl).origin;
|
|
285
|
+
if (url.origin !== gristOrigin) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`fetchWithGristAuth: refusing to send the Grist access token to ${url.origin}; absolute URLs must point at this document's Grist instance (${gristOrigin}). Use plain fetch() for other hosts.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (!url.searchParams.has("auth")) url.searchParams.set("auth", token.token);
|
|
291
|
+
const headers = new Headers(init?.headers ?? {});
|
|
292
|
+
headers.delete("Authorization");
|
|
293
|
+
const response = await fetch(url.toString(), { ...init, headers });
|
|
294
|
+
if (response.status === 401) {
|
|
295
|
+
clearGristAccessTokenCache();
|
|
296
|
+
const retryToken = await getGristAccessToken(docApi, { readOnly });
|
|
297
|
+
url.searchParams.set("auth", retryToken.token);
|
|
298
|
+
return fetch(url.toString(), { ...init, headers });
|
|
299
|
+
}
|
|
300
|
+
return response;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/sdk/helpers/grist-attachment-upload.ts
|
|
304
|
+
var ATTACHMENTS_COLUMN = { type: "Attachments" };
|
|
305
|
+
function parseGristAttachmentUploadResponseIds(raw, options) {
|
|
306
|
+
const trimmed = raw.trim();
|
|
307
|
+
if (!trimmed) {
|
|
308
|
+
throw new Error("Attachment upload returned an empty response.");
|
|
309
|
+
}
|
|
310
|
+
let parsed;
|
|
311
|
+
try {
|
|
312
|
+
parsed = JSON.parse(trimmed);
|
|
313
|
+
} catch {
|
|
314
|
+
if (options?.strict) {
|
|
315
|
+
throw new Error("Attachment upload returned non-JSON response.");
|
|
316
|
+
}
|
|
317
|
+
parsed = trimmed;
|
|
318
|
+
}
|
|
319
|
+
return extractAttachmentIdsFromUploadPayload(parsed, options);
|
|
320
|
+
}
|
|
321
|
+
function extractAttachmentIdsFromUploadPayload(parsed, options) {
|
|
322
|
+
if (options?.strict) {
|
|
323
|
+
return extractAttachmentIdsFromUploadPayloadStrict(parsed);
|
|
324
|
+
}
|
|
325
|
+
const first = extractFirstAttachmentIdFromUploadPayload(parsed);
|
|
326
|
+
if (first !== void 0) {
|
|
327
|
+
if (Array.isArray(parsed)) {
|
|
328
|
+
const ids = [];
|
|
329
|
+
const seen = /* @__PURE__ */ new Set();
|
|
330
|
+
for (const item of parsed) {
|
|
331
|
+
const id = coerceAttachmentId(item);
|
|
332
|
+
if (id && !seen.has(id)) {
|
|
333
|
+
seen.add(id);
|
|
334
|
+
ids.push(id);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return ids.length > 0 ? ids : [first];
|
|
338
|
+
}
|
|
339
|
+
return [first];
|
|
340
|
+
}
|
|
341
|
+
throw new Error("Attachment upload returned an unexpected response shape.");
|
|
342
|
+
}
|
|
343
|
+
function extractAttachmentIdsFromUploadPayloadStrict(parsed) {
|
|
344
|
+
if (typeof parsed === "number" && Number.isFinite(parsed)) {
|
|
345
|
+
return [String(parsed)];
|
|
346
|
+
}
|
|
347
|
+
if (Array.isArray(parsed)) {
|
|
348
|
+
const ids = [];
|
|
349
|
+
const seen = /* @__PURE__ */ new Set();
|
|
350
|
+
for (const item of parsed) {
|
|
351
|
+
if (typeof item !== "number" || !Number.isFinite(item)) {
|
|
352
|
+
throw new Error("Attachment upload returned an unexpected response shape.");
|
|
353
|
+
}
|
|
354
|
+
const id = String(item);
|
|
355
|
+
if (!seen.has(id)) {
|
|
356
|
+
seen.add(id);
|
|
357
|
+
ids.push(id);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (ids.length === 0) {
|
|
361
|
+
throw new Error("Attachment upload returned an empty id list.");
|
|
362
|
+
}
|
|
363
|
+
return ids;
|
|
364
|
+
}
|
|
365
|
+
throw new Error("Attachment upload returned an unexpected response shape.");
|
|
366
|
+
}
|
|
367
|
+
function extractFirstAttachmentIdFromUploadPayload(parsed) {
|
|
368
|
+
if (typeof parsed === "number" && Number.isFinite(parsed)) {
|
|
369
|
+
return String(parsed);
|
|
370
|
+
}
|
|
371
|
+
if (typeof parsed === "string" && parsed.trim()) {
|
|
372
|
+
return parsed.trim();
|
|
373
|
+
}
|
|
374
|
+
if (Array.isArray(parsed)) {
|
|
375
|
+
for (const item of parsed) {
|
|
376
|
+
const id = coerceAttachmentId(item);
|
|
377
|
+
if (id) return id;
|
|
378
|
+
}
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
if (parsed && typeof parsed === "object") {
|
|
382
|
+
const record = parsed;
|
|
383
|
+
for (const key of ["id", "attachmentId", "fileId"]) {
|
|
384
|
+
const id = coerceAttachmentId(record[key]);
|
|
385
|
+
if (id) return id;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return void 0;
|
|
389
|
+
}
|
|
390
|
+
function coerceAttachmentId(value) {
|
|
391
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
392
|
+
return String(value);
|
|
393
|
+
}
|
|
394
|
+
if (typeof value === "string" && value.trim()) {
|
|
395
|
+
return value.trim();
|
|
396
|
+
}
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
function normalizeAttachmentIdForWire(id) {
|
|
400
|
+
const numeric = typeof id === "number" ? id : Number(id);
|
|
401
|
+
if (Number.isFinite(numeric) && String(numeric) === String(id).trim()) {
|
|
402
|
+
return numeric;
|
|
403
|
+
}
|
|
404
|
+
return typeof id === "number" ? String(id) : id.trim();
|
|
405
|
+
}
|
|
406
|
+
function gristAttachmentCellValues(attachmentIds) {
|
|
407
|
+
const items = attachmentIds.map(normalizeAttachmentIdForWire);
|
|
408
|
+
const encoded = encodeGristValue(items, ATTACHMENTS_COLUMN);
|
|
409
|
+
if (!Array.isArray(encoded) || encoded[0] !== "L") {
|
|
410
|
+
return ["L", ...items];
|
|
411
|
+
}
|
|
412
|
+
return encoded;
|
|
413
|
+
}
|
|
414
|
+
function gristAttachmentCellValue(attachmentId) {
|
|
415
|
+
return gristAttachmentCellValues([attachmentId]);
|
|
416
|
+
}
|
|
417
|
+
function mergeGristAttachmentCellValue(existingCell, newIds) {
|
|
418
|
+
const existing = extractGristAttachmentIdsFromCell(existingCell);
|
|
419
|
+
const merged = [];
|
|
420
|
+
const seen = /* @__PURE__ */ new Set();
|
|
421
|
+
const push = (raw) => {
|
|
422
|
+
const id = String(raw).trim();
|
|
423
|
+
if (!id || seen.has(id)) return;
|
|
424
|
+
seen.add(id);
|
|
425
|
+
merged.push(id);
|
|
426
|
+
};
|
|
427
|
+
for (const id of existing) push(id);
|
|
428
|
+
for (const id of newIds) push(id);
|
|
429
|
+
return gristAttachmentCellValues(merged);
|
|
430
|
+
}
|
|
431
|
+
var UPLOAD_HEADERS = {
|
|
432
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
433
|
+
Accept: "application/json"
|
|
434
|
+
};
|
|
435
|
+
async function uploadGristAttachment(docApi, file, options) {
|
|
436
|
+
const form = new FormData();
|
|
437
|
+
const fileName = options?.fileName ?? (file instanceof File && file.name ? file.name : "upload");
|
|
438
|
+
form.append("upload", file, fileName);
|
|
439
|
+
let response;
|
|
440
|
+
try {
|
|
441
|
+
response = await fetchWithGristAuth(docApi, "/attachments", {
|
|
442
|
+
method: "POST",
|
|
443
|
+
body: form,
|
|
444
|
+
readOnly: false,
|
|
445
|
+
headers: UPLOAD_HEADERS
|
|
446
|
+
});
|
|
447
|
+
} catch (err) {
|
|
448
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
449
|
+
if (/failed to fetch/i.test(msg)) {
|
|
450
|
+
const widgetOrigin = typeof globalThis.location !== "undefined" ? globalThis.location.origin : "this widget origin";
|
|
451
|
+
throw new Error(
|
|
452
|
+
`Attachment upload was blocked by the Grist host (CORS). The browser could not POST from ${widgetOrigin} to the document API. If upload worked from a dev URL (e.g. ngrok) but fails after deploy, add the new widget URL to your Grist server CORS allowlist (see grist-core#2040).`,
|
|
453
|
+
{ cause: err }
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
throw err;
|
|
457
|
+
}
|
|
458
|
+
const raw = await response.text();
|
|
459
|
+
if (!response.ok) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
raw.trim() ? `Attachment upload failed (${response.status}): ${raw.slice(0, 200)}` : `Attachment upload failed (${response.status})`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
const ids = parseGristAttachmentUploadResponseIds(raw);
|
|
465
|
+
return { ids, firstId: ids[0] };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/sdk/runtime/cells.ts
|
|
469
|
+
var ok = (value) => ({ ok: true, value });
|
|
470
|
+
var bad = (message2) => ({ ok: false, message: message2 });
|
|
471
|
+
function formulaError(value) {
|
|
472
|
+
if (Array.isArray(value) && value[0] === "E") return String(value[1] ?? "error");
|
|
473
|
+
if (value && typeof value === "object" && value.constructor?.name === "RaisedException") {
|
|
474
|
+
return String(value.name ?? "error");
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
function rowIdOf(value) {
|
|
479
|
+
if (typeof value === "number" && Number.isInteger(value)) return value === 0 ? null : value;
|
|
480
|
+
if (value && typeof value === "object" && typeof value.rowId === "number") {
|
|
481
|
+
const id = value.rowId;
|
|
482
|
+
return id === 0 ? null : id;
|
|
483
|
+
}
|
|
484
|
+
if (Array.isArray(value) && value[0] === "R" && typeof value[2] === "number") return value[2] === 0 ? null : value[2];
|
|
485
|
+
return void 0;
|
|
486
|
+
}
|
|
487
|
+
function idsOf(value) {
|
|
488
|
+
if (value === null || value === void 0) return [];
|
|
489
|
+
const list = Array.isArray(value) ? value[0] === "L" ? value.slice(1) : value : value && typeof value === "object" && Array.isArray(value.rowIds) ? value.rowIds : void 0;
|
|
490
|
+
return list?.every((v) => typeof v === "number") ? list : void 0;
|
|
491
|
+
}
|
|
492
|
+
function checkOne(base, value) {
|
|
493
|
+
switch (base) {
|
|
494
|
+
case "Any":
|
|
495
|
+
return ok(value);
|
|
496
|
+
case "Text":
|
|
497
|
+
return value === null || typeof value === "string" ? ok(value ?? "") : bad("not text");
|
|
498
|
+
case "Numeric":
|
|
499
|
+
return value === null || typeof value === "number" && Number.isFinite(value) ? ok(value) : bad("not a number");
|
|
500
|
+
case "Int":
|
|
501
|
+
return value === null || typeof value === "number" && Number.isInteger(value) ? ok(value) : bad("not an integer");
|
|
502
|
+
case "Bool":
|
|
503
|
+
return value === null || typeof value === "boolean" ? ok(value ?? false) : bad("not true or false");
|
|
504
|
+
case "Date":
|
|
505
|
+
case "DateTime":
|
|
506
|
+
if (value === null) return ok(null);
|
|
507
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return ok(value);
|
|
508
|
+
if (typeof value === "number" && Number.isFinite(value)) return ok(new Date(value * 1e3));
|
|
509
|
+
return bad("not a date");
|
|
510
|
+
case "Choice":
|
|
511
|
+
return value === null || typeof value === "string" ? ok(value) : bad("not a choice");
|
|
512
|
+
case "ChoiceList": {
|
|
513
|
+
if (value === null || value === void 0) return ok([]);
|
|
514
|
+
const list = Array.isArray(value) ? value[0] === "L" ? value.slice(1) : value : void 0;
|
|
515
|
+
return list?.every((v) => typeof v === "string") ? ok(list) : bad("not a list of choices");
|
|
516
|
+
}
|
|
517
|
+
case "Ref": {
|
|
518
|
+
if (value === null) return ok(null);
|
|
519
|
+
const id = rowIdOf(value);
|
|
520
|
+
return id === void 0 ? bad("not a reference") : ok(id);
|
|
521
|
+
}
|
|
522
|
+
case "RefList":
|
|
523
|
+
case "Attachments": {
|
|
524
|
+
const ids = idsOf(value);
|
|
525
|
+
return ids === void 0 ? bad(base === "RefList" ? "not a list of references" : "not a list of attachments") : ok(ids);
|
|
526
|
+
}
|
|
527
|
+
default:
|
|
528
|
+
return ok(value);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function checkGristCell(type, value) {
|
|
532
|
+
const error = formulaError(value);
|
|
533
|
+
if (error) return bad(`formula error (${error})`);
|
|
534
|
+
const bases = (type ?? "Any").split(",").map((t) => t.trim().split(":")[0]).filter(Boolean);
|
|
535
|
+
if (bases.length === 0 || bases.includes("Any")) return ok(value === void 0 ? null : value);
|
|
536
|
+
let first = null;
|
|
537
|
+
for (const base of bases) {
|
|
538
|
+
const check = checkOne(base, value);
|
|
539
|
+
if (check.ok) return check;
|
|
540
|
+
first ?? (first = check);
|
|
541
|
+
}
|
|
542
|
+
return first;
|
|
543
|
+
}
|
|
544
|
+
var nameOf = (c) => typeof c === "string" ? c : c.name;
|
|
545
|
+
var typeOf = (c) => typeof c === "string" ? void 0 : c.type;
|
|
546
|
+
var isOptional = (c) => typeof c === "object" && c.optional === true;
|
|
547
|
+
function mapAndCheck(record, columns, mappings, schema) {
|
|
548
|
+
if (!record) return { value: null, issues: [] };
|
|
549
|
+
if (!columns?.length) return { value: record, issues: [] };
|
|
550
|
+
if (mappings === null) return { value: null, issues: [] };
|
|
551
|
+
const rowId = typeof record.id === "number" ? record.id : 0;
|
|
552
|
+
const out = { id: record.id };
|
|
553
|
+
const issues = [];
|
|
554
|
+
for (const col of columns) {
|
|
555
|
+
const name = nameOf(col);
|
|
556
|
+
const real = mappings[name];
|
|
557
|
+
const take = (raw) => {
|
|
558
|
+
const check = checkGristCell(typeOf(col), raw);
|
|
559
|
+
if (check.ok) return check.value;
|
|
560
|
+
issues.push({ rowId, column: name, message: check.message, value: raw });
|
|
561
|
+
return null;
|
|
562
|
+
};
|
|
563
|
+
if (Array.isArray(real) && real.length > 0) out[name] = real.map((r) => take(record[r]));
|
|
564
|
+
else if (typeof real === "string" && real) out[name] = take(record[real]);
|
|
565
|
+
else if (!isOptional(col)) return { value: null, issues: [] };
|
|
566
|
+
}
|
|
567
|
+
if (schema) {
|
|
568
|
+
const result = schema["~standard"].validate(out);
|
|
569
|
+
if (result instanceof Promise) {
|
|
570
|
+
issues.push({ rowId, column: "", message: "the schema validates asynchronously; useGrist() needs a synchronous one", value: out });
|
|
571
|
+
} else if ("issues" in result && result.issues) {
|
|
572
|
+
for (const issue of result.issues) {
|
|
573
|
+
const key = issue.path?.[0];
|
|
574
|
+
const column = key === void 0 ? "" : String(typeof key === "object" ? key.key : key);
|
|
575
|
+
issues.push({ rowId, column, message: issue.message, value: column ? out[column] : out });
|
|
576
|
+
}
|
|
577
|
+
} else if ("value" in result) {
|
|
578
|
+
return { value: result.value, issues };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return { value: out, issues };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/sdk/use-grist.ts
|
|
585
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
586
|
+
|
|
587
|
+
// src/sdk/helpers/build-replica-document.ts
|
|
588
|
+
var FORMULA_PREVIEW_MAX = 500;
|
|
589
|
+
function readCellStr(table, key, rowIndex) {
|
|
590
|
+
const col = table[key];
|
|
591
|
+
if (!Array.isArray(col)) return void 0;
|
|
592
|
+
const v = col[rowIndex];
|
|
593
|
+
if (v === null || v === void 0) return void 0;
|
|
594
|
+
if (typeof v === "string") return v;
|
|
595
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
596
|
+
return void 0;
|
|
597
|
+
}
|
|
598
|
+
function readCellNum(table, key, rowIndex) {
|
|
599
|
+
const col = table[key];
|
|
600
|
+
if (!Array.isArray(col)) return void 0;
|
|
601
|
+
const v = col[rowIndex];
|
|
602
|
+
if (typeof v === "number" && !Number.isNaN(v)) return v;
|
|
603
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
604
|
+
const n = Number(v);
|
|
605
|
+
if (!Number.isNaN(n)) return n;
|
|
606
|
+
}
|
|
607
|
+
return void 0;
|
|
608
|
+
}
|
|
609
|
+
function pickColumnMeta(meta) {
|
|
610
|
+
if (!meta) return {};
|
|
611
|
+
const out = {};
|
|
612
|
+
if (meta.colRef !== void 0) out.colRef = meta.colRef;
|
|
613
|
+
if (meta.type) out.type = meta.type;
|
|
614
|
+
if (meta.label) out.label = meta.label;
|
|
615
|
+
if (meta.parentPos !== void 0) out.parentPos = meta.parentPos;
|
|
616
|
+
if (meta.isFormula) out.isFormula = true;
|
|
617
|
+
const formulaRaw = meta.formula?.trim() ?? "";
|
|
618
|
+
out.formulaKind = inferGristFormulaKind(Boolean(meta.isFormula), formulaRaw);
|
|
619
|
+
if (formulaRaw) {
|
|
620
|
+
out.formula = formulaRaw.length > FORMULA_PREVIEW_MAX ? `${formulaRaw.slice(0, FORMULA_PREVIEW_MAX)}...` : formulaRaw;
|
|
621
|
+
}
|
|
622
|
+
if (meta.description) out.description = meta.description;
|
|
623
|
+
if (meta.widgetOptions && Object.keys(meta.widgetOptions).length > 0) {
|
|
624
|
+
out.widgetOptions = meta.widgetOptions;
|
|
625
|
+
}
|
|
626
|
+
if (meta.displayColRef !== void 0) out.displayColRef = meta.displayColRef;
|
|
627
|
+
if (meta.visibleColRef !== void 0) out.visibleColRef = meta.visibleColRef;
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
async function loadColumnMetadataByTable(docApi) {
|
|
631
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
632
|
+
try {
|
|
633
|
+
const tables = await docApi.fetchTable("_grist_Tables");
|
|
634
|
+
const columns = await docApi.fetchTable("_grist_Tables_column");
|
|
635
|
+
const tableIds = tables.tableId;
|
|
636
|
+
const tableInternalIds = tables.id;
|
|
637
|
+
if (!Array.isArray(tableIds) || !Array.isArray(tableInternalIds)) return byTable;
|
|
638
|
+
const internalIdToTableId = /* @__PURE__ */ new Map();
|
|
639
|
+
for (let i = 0; i < tableInternalIds.length; i++) {
|
|
640
|
+
const tid = tableIds[i];
|
|
641
|
+
const iid = coerceGristDocApiRefRowId(tableInternalIds[i]);
|
|
642
|
+
if (typeof tid === "string" && iid !== void 0) internalIdToTableId.set(iid, tid);
|
|
643
|
+
}
|
|
644
|
+
const parentIds = columns.parentId;
|
|
645
|
+
const colIds = columns.colId;
|
|
646
|
+
const columnIds = columns.id;
|
|
647
|
+
if (!Array.isArray(parentIds) || !Array.isArray(colIds) || !Array.isArray(columnIds) || parentIds.length !== colIds.length || colIds.length !== columnIds.length) {
|
|
648
|
+
return byTable;
|
|
649
|
+
}
|
|
650
|
+
const n = parentIds.length;
|
|
651
|
+
for (let i = 0; i < n; i++) {
|
|
652
|
+
const parentRowId = coerceGristDocApiRefRowId(parentIds[i]);
|
|
653
|
+
const colRef = coerceGristDocApiRefRowId(columnIds[i]);
|
|
654
|
+
const colId = colIds[i];
|
|
655
|
+
if (parentRowId === void 0 || colRef === void 0 || typeof colId !== "string") continue;
|
|
656
|
+
const tableId = internalIdToTableId.get(parentRowId);
|
|
657
|
+
if (!tableId) continue;
|
|
658
|
+
let colMap = byTable.get(tableId);
|
|
659
|
+
if (!colMap) {
|
|
660
|
+
colMap = /* @__PURE__ */ new Map();
|
|
661
|
+
byTable.set(tableId, colMap);
|
|
662
|
+
}
|
|
663
|
+
const parentPos = readCellNum(columns, "parentPos", i);
|
|
664
|
+
const isFormulaCol = columns.isFormula;
|
|
665
|
+
const isFormula = Array.isArray(isFormulaCol) ? Boolean(isFormulaCol[i]) : false;
|
|
666
|
+
const woCol = columns.widgetOptions;
|
|
667
|
+
const widgetOptions = Array.isArray(woCol) && i < woCol.length ? parseGristWidgetOptionsCell(woCol[i]) : void 0;
|
|
668
|
+
const displayColRef = coerceGristDocApiRefRowId(
|
|
669
|
+
Array.isArray(columns.displayCol) ? columns.displayCol[i] : void 0
|
|
670
|
+
);
|
|
671
|
+
const visibleColRef = coerceGristDocApiRefRowId(
|
|
672
|
+
Array.isArray(columns.visibleCol) ? columns.visibleCol[i] : void 0
|
|
673
|
+
);
|
|
674
|
+
colMap.set(colId, {
|
|
675
|
+
colRef,
|
|
676
|
+
type: readCellStr(columns, "type", i),
|
|
677
|
+
label: readCellStr(columns, "label", i),
|
|
678
|
+
parentPos: parentPos !== void 0 ? parentPos : void 0,
|
|
679
|
+
isFormula,
|
|
680
|
+
formula: readCellStr(columns, "formula", i),
|
|
681
|
+
description: readCellStr(columns, "description", i) ?? readCellStr(columns, "descr", i),
|
|
682
|
+
widgetOptions,
|
|
683
|
+
displayColRef,
|
|
684
|
+
visibleColRef
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
return byTable;
|
|
690
|
+
}
|
|
691
|
+
function orderedColumnIds(data, metaByCol) {
|
|
692
|
+
const keys = Object.keys(data).filter((k) => Array.isArray(data[k]));
|
|
693
|
+
keys.sort((a, b) => {
|
|
694
|
+
const pa = metaByCol.get(a)?.parentPos;
|
|
695
|
+
const pb = metaByCol.get(b)?.parentPos;
|
|
696
|
+
if (pa !== void 0 && pb !== void 0 && pa !== pb) return pa - pb;
|
|
697
|
+
if (pa !== void 0 && pb === void 0) return -1;
|
|
698
|
+
if (pa === void 0 && pb !== void 0) return 1;
|
|
699
|
+
return a.localeCompare(b);
|
|
700
|
+
});
|
|
701
|
+
return keys;
|
|
702
|
+
}
|
|
703
|
+
function buildReplicaColumnsFromColumnar(data, colMeta) {
|
|
704
|
+
const columnIds = orderedColumnIds(data, colMeta);
|
|
705
|
+
const columns = {};
|
|
706
|
+
for (const colId of columnIds) {
|
|
707
|
+
const arr = data[colId];
|
|
708
|
+
if (!Array.isArray(arr)) continue;
|
|
709
|
+
const meta = colMeta.get(colId);
|
|
710
|
+
columns[colId] = pickColumnMeta(meta);
|
|
711
|
+
}
|
|
712
|
+
return columns;
|
|
713
|
+
}
|
|
714
|
+
function rowModeFromIncludeRows(includeRows) {
|
|
715
|
+
if (includeRows === false) return "schema-only";
|
|
716
|
+
if (includeRows === "samples") return "schema+samples";
|
|
717
|
+
return "schema+data";
|
|
718
|
+
}
|
|
719
|
+
async function buildReplicaDocumentFromDocApi(docApi, options) {
|
|
720
|
+
const includeSystemTableData = options?.includeSystemTableData ?? false;
|
|
721
|
+
const includeRows = options?.includeRows ?? "samples";
|
|
722
|
+
const sampleRowLimit = options?.sampleRowLimit ?? 10;
|
|
723
|
+
const rowMode = rowModeFromIncludeRows(includeRows);
|
|
724
|
+
const metaByTable = await loadColumnMetadataByTable(docApi);
|
|
725
|
+
let docName;
|
|
726
|
+
try {
|
|
727
|
+
docName = String(await Promise.resolve(docApi.getDocName()));
|
|
728
|
+
} catch {
|
|
729
|
+
docName = void 0;
|
|
730
|
+
}
|
|
731
|
+
const tableIdsRaw = await Promise.resolve(docApi.listTables());
|
|
732
|
+
const tableIds = Array.isArray(tableIdsRaw) ? [...tableIdsRaw] : [];
|
|
733
|
+
const tables = {};
|
|
734
|
+
for (const tableId of tableIds) {
|
|
735
|
+
const omitData = !includeSystemTableData && tableId.startsWith("_grist");
|
|
736
|
+
if (omitData) {
|
|
737
|
+
tables[tableId] = {
|
|
738
|
+
rowCount: 0,
|
|
739
|
+
dataOmitted: true,
|
|
740
|
+
dataOmittedReason: "Row scan skipped for tables whose id starts with `_grist` unless `includeSystemTableData` is true.",
|
|
741
|
+
columns: {}
|
|
742
|
+
};
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
try {
|
|
746
|
+
const data = await docApi.fetchTable(tableId);
|
|
747
|
+
const rowCount = computeRowCount(data);
|
|
748
|
+
const colMeta = metaByTable.get(tableId) ?? /* @__PURE__ */ new Map();
|
|
749
|
+
const columnIds = orderedColumnIds(data, colMeta);
|
|
750
|
+
const columns = buildReplicaColumnsFromColumnar(data, colMeta);
|
|
751
|
+
const table = { columns, rowCount };
|
|
752
|
+
if (includeRows === "full") {
|
|
753
|
+
table.rows = decodeColumnarToReplicaRows(data, columnIds, columns);
|
|
754
|
+
} else if (includeRows === "samples") {
|
|
755
|
+
table.rows = decodeColumnarToReplicaRows(data, columnIds, columns, sampleRowLimit);
|
|
756
|
+
}
|
|
757
|
+
tables[tableId] = table;
|
|
758
|
+
} catch (err) {
|
|
759
|
+
tables[tableId] = {
|
|
760
|
+
rowCount: 0,
|
|
761
|
+
error: err instanceof Error ? err.message : String(err),
|
|
762
|
+
columns: {}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const out = {
|
|
767
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
768
|
+
docName,
|
|
769
|
+
mode: rowMode,
|
|
770
|
+
tables
|
|
771
|
+
};
|
|
772
|
+
if (options?.source !== void 0 && options.source !== "") {
|
|
773
|
+
out.source = options.source;
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// src/sdk/helpers/grist-columns-filters.ts
|
|
779
|
+
var NOISE_COLUMN_FORMULAS = /* @__PURE__ */ new Set(["now()"]);
|
|
780
|
+
function normalizeFormulaForComparison(formula) {
|
|
781
|
+
return formula.trim().toLowerCase().replace(/\s+/g, "");
|
|
782
|
+
}
|
|
783
|
+
function isGristNoiseColumn(colId, col, options) {
|
|
784
|
+
const excludeId = options?.excludeId ?? true;
|
|
785
|
+
const excludeGristHelpers = options?.excludeGristHelpers ?? true;
|
|
786
|
+
const excludeLoggingColumns = options?.excludeLoggingColumns ?? true;
|
|
787
|
+
const excludeManualSort = options?.excludeManualSort ?? false;
|
|
788
|
+
if (excludeId && colId === "id") return true;
|
|
789
|
+
if (excludeManualSort && colId === "manualSort") return true;
|
|
790
|
+
if (excludeGristHelpers && colId.startsWith("gristHelper_")) return true;
|
|
791
|
+
if (excludeLoggingColumns) {
|
|
792
|
+
const formula = typeof col?.formula === "string" ? normalizeFormulaForComparison(col.formula) : "";
|
|
793
|
+
if (formula && NOISE_COLUMN_FORMULAS.has(formula)) return true;
|
|
794
|
+
if (formula.startsWith("user.")) return true;
|
|
795
|
+
}
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/sdk/runtime/api.ts
|
|
800
|
+
var ACCESS_ERROR = [/access not granted/i, /need higher access level/i, /requires.*full.*access/i, /RPC_ONCALL_ERROR Access/i];
|
|
801
|
+
function isAccessInsufficientError(message2) {
|
|
802
|
+
return ACCESS_ERROR.some((re) => re.test(message2));
|
|
803
|
+
}
|
|
804
|
+
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
805
|
+
function createGristApi(runtime) {
|
|
806
|
+
const docApi = () => {
|
|
807
|
+
if (runtime.getState().connection !== "ready" || typeof grist === "undefined" || !grist.docApi) {
|
|
808
|
+
throw new Error('Grist is not ready: wait for useGrist().status === "ready".');
|
|
809
|
+
}
|
|
810
|
+
return grist.docApi;
|
|
811
|
+
};
|
|
812
|
+
const read = async (fn) => {
|
|
813
|
+
try {
|
|
814
|
+
const out = await fn();
|
|
815
|
+
if (runtime.getState().readError) runtime.update({ readError: null });
|
|
816
|
+
return out;
|
|
817
|
+
} catch (err) {
|
|
818
|
+
if (isAccessInsufficientError(message(err))) runtime.update({ readError: message(err) });
|
|
819
|
+
throw err;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
const write = async (fn) => {
|
|
823
|
+
runtime.update({ actionStatus: "running", actionError: null });
|
|
824
|
+
try {
|
|
825
|
+
const out = await fn();
|
|
826
|
+
runtime.update({ actionStatus: "idle" });
|
|
827
|
+
return out;
|
|
828
|
+
} catch (err) {
|
|
829
|
+
runtime.update({ actionStatus: "error", actionError: message(err) });
|
|
830
|
+
throw err;
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
const tableOps = (resolve) => ({
|
|
834
|
+
getTableId: () => resolve().getTableId(),
|
|
835
|
+
create: ((records, opts) => write(() => resolve().create(records, opts))),
|
|
836
|
+
update: (records, opts) => write(() => resolve().update(records, opts)),
|
|
837
|
+
upsert: (records, opts) => write(() => resolve().upsert(records, opts)),
|
|
838
|
+
destroy: (ids) => write(() => resolve().destroy(ids))
|
|
839
|
+
});
|
|
840
|
+
const tables = /* @__PURE__ */ new Map();
|
|
841
|
+
return {
|
|
842
|
+
reload: () => runtime.reload(),
|
|
843
|
+
fetchSelectedTable: (options) => read(async () => {
|
|
844
|
+
const impl = docApi().fetchSelectedTable ?? grist.fetchSelectedTable;
|
|
845
|
+
if (!impl) throw new Error("fetchSelectedTable is not available in this Grist version");
|
|
846
|
+
return await impl({ keepEncoded: false, ...options });
|
|
847
|
+
}),
|
|
848
|
+
fetchTable: (tableId) => read(async () => await docApi().fetchTable(tableId)),
|
|
849
|
+
fetchTableRows: (tableId, options) => read(() => fetchGristTableRows(docApi(), tableId, options ?? {})),
|
|
850
|
+
listTables: (options) => read(async () => {
|
|
851
|
+
const all = [...await docApi().listTables()];
|
|
852
|
+
return options?.includeSystem ? all : all.filter((t) => !t.startsWith("_grist") && !t.startsWith("GristHidden_"));
|
|
853
|
+
}),
|
|
854
|
+
listColumns: (tableId, options) => read(async () => {
|
|
855
|
+
const meta = (await loadColumnMetadataByTable(docApi())).get(tableId);
|
|
856
|
+
if (!meta) return [];
|
|
857
|
+
const noise = {
|
|
858
|
+
excludeId: options?.excludeId ?? true,
|
|
859
|
+
excludeGristHelpers: options?.excludeGristHelpers ?? true,
|
|
860
|
+
excludeLoggingColumns: options?.excludeLoggingColumns ?? true,
|
|
861
|
+
excludeManualSort: options?.excludeManualSort ?? true
|
|
862
|
+
};
|
|
863
|
+
return [...meta].filter(([colId, m]) => options?.includeSystem || !isGristNoiseColumn(colId, m, noise)).map(([colId, m]) => ({ colId, type: m.type, label: m.label, isFormula: m.isFormula, formula: m.formula, description: m.description }));
|
|
864
|
+
}),
|
|
865
|
+
table: tableOps(() => {
|
|
866
|
+
if (!grist.selectedTable) throw new Error("grist.selectedTable is not available in this Grist version");
|
|
867
|
+
return grist.selectedTable;
|
|
868
|
+
}),
|
|
869
|
+
getTable: (tableId) => {
|
|
870
|
+
let ops = tables.get(tableId);
|
|
871
|
+
if (!ops) {
|
|
872
|
+
tables.set(
|
|
873
|
+
tableId,
|
|
874
|
+
ops = tableOps(() => {
|
|
875
|
+
if (typeof grist.getTable !== "function") throw new Error("grist.getTable is not available in this Grist version");
|
|
876
|
+
return grist.getTable(tableId);
|
|
877
|
+
})
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
return ops;
|
|
881
|
+
},
|
|
882
|
+
applyActions: (actions, options) => write(() => docApi().applyUserActions(actions, options)),
|
|
883
|
+
setWidgetOptions: async (options) => {
|
|
884
|
+
await grist.setOptions(options);
|
|
885
|
+
},
|
|
886
|
+
patchWidgetOptions: async (patch) => {
|
|
887
|
+
const current = await grist.getOptions() ?? {};
|
|
888
|
+
await grist.setOptions({ ...current, ...patch });
|
|
889
|
+
},
|
|
890
|
+
setCursorPosition: async (pos) => {
|
|
891
|
+
if (!grist.setCursorPos) throw new Error("setCursorPos is not available in this Grist version");
|
|
892
|
+
await grist.setCursorPos(pos);
|
|
893
|
+
},
|
|
894
|
+
setLinkedRowSelection: async (rowIds) => {
|
|
895
|
+
if (!grist.setSelectedRows) throw new Error("setSelectedRows is not available in this Grist version");
|
|
896
|
+
await grist.setSelectedRows(rowIds);
|
|
897
|
+
},
|
|
898
|
+
getAttachmentUrl: (id, options) => read(() => getAttachmentDownloadUrl(docApi(), id, options)),
|
|
899
|
+
fetchAttachmentBlob: (id, options) => read(() => fetchAttachmentBlob(docApi(), id, options)),
|
|
900
|
+
uploadAttachment: (file, options) => write(() => uploadGristAttachment(docApi(), file, options)),
|
|
901
|
+
fetchWithAuth: (path, init) => read(() => fetchWithGristAuth(docApi(), path, init))
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/sdk/helpers/grist-mappings.ts
|
|
906
|
+
function asColumnSpec(entry) {
|
|
907
|
+
return typeof entry === "string" ? { name: entry } : entry;
|
|
908
|
+
}
|
|
909
|
+
function validateColumnMappings(spec, mappings) {
|
|
910
|
+
const missing = [];
|
|
911
|
+
const emptyMultiples = [];
|
|
912
|
+
if (!spec) return { ok: true, pending: false, missing, emptyMultiples };
|
|
913
|
+
const m = mappings ?? {};
|
|
914
|
+
for (const raw of spec) {
|
|
915
|
+
const col = asColumnSpec(raw);
|
|
916
|
+
if (col.optional) continue;
|
|
917
|
+
const value = m[col.name];
|
|
918
|
+
if (col.allowMultiple) {
|
|
919
|
+
if (value === void 0) {
|
|
920
|
+
missing.push(col.name);
|
|
921
|
+
} else if (!Array.isArray(value) || value.length === 0) {
|
|
922
|
+
emptyMultiples.push(col.name);
|
|
923
|
+
}
|
|
924
|
+
} else {
|
|
925
|
+
if (value === void 0 || value === null || value === "") missing.push(col.name);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
ok: missing.length === 0 && emptyMultiples.length === 0,
|
|
930
|
+
pending: false,
|
|
931
|
+
missing,
|
|
932
|
+
emptyMultiples
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/sdk/runtime/derive.ts
|
|
937
|
+
var ACCESS_RANK = { none: 0, "read table": 1, full: 2 };
|
|
938
|
+
function parseAccess(raw) {
|
|
939
|
+
return raw === "none" || raw === "read table" || raw === "full" ? raw : null;
|
|
940
|
+
}
|
|
941
|
+
function accessLabel(level) {
|
|
942
|
+
return level === "full" ? "Full document access" : level === "read table" ? "Read selected table" : "No document access";
|
|
943
|
+
}
|
|
944
|
+
function accessDenied(state) {
|
|
945
|
+
return state.grantedAccess !== null && ACCESS_RANK[state.grantedAccess] < ACCESS_RANK[state.declaredAccess];
|
|
946
|
+
}
|
|
947
|
+
function deriveStatus(state) {
|
|
948
|
+
switch (state.connection) {
|
|
949
|
+
case "connecting":
|
|
950
|
+
return "booting";
|
|
951
|
+
case "not-embedded":
|
|
952
|
+
case "unavailable":
|
|
953
|
+
return "unavailable";
|
|
954
|
+
case "error":
|
|
955
|
+
return "error";
|
|
956
|
+
case "ready":
|
|
957
|
+
return accessDenied(state) ? "error" : "ready";
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function deriveError(state) {
|
|
961
|
+
if (state.connection === "not-embedded") return "This page is not embedded in a Grist document.";
|
|
962
|
+
if (state.connection === "unavailable") return "The Grist plugin API (grist-plugin-api.js) did not load.";
|
|
963
|
+
if (state.connection === "error") return state.connectionError;
|
|
964
|
+
if (state.connection === "ready" && accessDenied(state)) {
|
|
965
|
+
return `Grist granted "${accessLabel(state.grantedAccess)}"; this widget needs "${accessLabel(state.declaredAccess)}". Set the access level in the Custom widget settings.`;
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
var MAPPINGS_OK = Object.freeze({ ok: true, pending: false, missing: [], emptyMultiples: [] });
|
|
970
|
+
var MAPPINGS_PENDING = Object.freeze({ ok: true, pending: true, missing: [], emptyMultiples: [] });
|
|
971
|
+
function deriveColumnMappingStatus(columns, mappings) {
|
|
972
|
+
if (!columns?.length) return MAPPINGS_OK;
|
|
973
|
+
if (mappings === null) return MAPPINGS_PENDING;
|
|
974
|
+
return validateColumnMappings(columns, mappings);
|
|
975
|
+
}
|
|
976
|
+
function mapBackPatch(patch, mappings) {
|
|
977
|
+
const fields = {};
|
|
978
|
+
const skipped = [];
|
|
979
|
+
for (const [name, value] of Object.entries(patch)) {
|
|
980
|
+
if (value === void 0) continue;
|
|
981
|
+
const real = mappings[name];
|
|
982
|
+
if (Array.isArray(real)) skipped.push(name);
|
|
983
|
+
else fields[typeof real === "string" && real ? real : name] = value;
|
|
984
|
+
}
|
|
985
|
+
return { fields, skipped };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/sdk/helpers/grist-widget-interaction.ts
|
|
989
|
+
function parseGristWidgetLinkingInfo(raw) {
|
|
990
|
+
if (!raw || typeof raw !== "object" || !("asTarget" in raw)) return void 0;
|
|
991
|
+
const linking = raw;
|
|
992
|
+
return {
|
|
993
|
+
asTarget: linking.asTarget == null ? null : String(linking.asTarget),
|
|
994
|
+
asSource: Boolean(linking.asSource)
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
function normalizeWidgetInteraction(interaction) {
|
|
998
|
+
if (!interaction || typeof interaction !== "object") return interaction;
|
|
999
|
+
const raw = interaction;
|
|
1000
|
+
const access_level = typeof raw.access_level === "string" ? raw.access_level : typeof raw.accessLevel === "string" ? raw.accessLevel : void 0;
|
|
1001
|
+
const linking = parseGristWidgetLinkingInfo(raw.linking);
|
|
1002
|
+
return {
|
|
1003
|
+
...interaction,
|
|
1004
|
+
...access_level ? { access_level } : {},
|
|
1005
|
+
...linking ? { linking } : {}
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/sdk/runtime/host.ts
|
|
1010
|
+
var HostStream = class {
|
|
1011
|
+
constructor(bind) {
|
|
1012
|
+
this.bind = bind;
|
|
1013
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1014
|
+
this.bound = false;
|
|
1015
|
+
this.has = false;
|
|
1016
|
+
}
|
|
1017
|
+
subscribe(listener) {
|
|
1018
|
+
this.listeners.add(listener);
|
|
1019
|
+
if (!this.bound) {
|
|
1020
|
+
this.bound = true;
|
|
1021
|
+
try {
|
|
1022
|
+
this.bind((value) => this.emit(value));
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
console.error("[grist-widget-sdk] could not subscribe to a Grist event:", err);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (this.has) listener(this.last);
|
|
1028
|
+
return () => {
|
|
1029
|
+
this.listeners.delete(listener);
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
peek() {
|
|
1033
|
+
return this.last;
|
|
1034
|
+
}
|
|
1035
|
+
/** Tests: deliver a value as if Grist had sent it. */
|
|
1036
|
+
emit(value) {
|
|
1037
|
+
this.has = true;
|
|
1038
|
+
this.last = value;
|
|
1039
|
+
for (const l of [...this.listeners]) l(value);
|
|
1040
|
+
}
|
|
1041
|
+
/** Tests: forget the binding, so a new `window.grist` binds afresh. */
|
|
1042
|
+
reset() {
|
|
1043
|
+
this.listeners.clear();
|
|
1044
|
+
this.bound = false;
|
|
1045
|
+
this.has = false;
|
|
1046
|
+
this.last = void 0;
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
var asMappings = (m) => m && typeof m === "object" ? m : null;
|
|
1050
|
+
var recordStream = new HostStream((emit) => {
|
|
1051
|
+
grist.onRecord((record, mappings) => emit({ record, mappings: asMappings(mappings) }), { keepEncoded: false });
|
|
1052
|
+
const fetchSelectedRecord = grist.fetchSelectedRecord ?? grist.docApi?.fetchSelectedRecord;
|
|
1053
|
+
if (typeof grist.on !== "function" || typeof fetchSelectedRecord !== "function") return;
|
|
1054
|
+
let lastRowId;
|
|
1055
|
+
grist.on("message", async (msg) => {
|
|
1056
|
+
const host = msg;
|
|
1057
|
+
if (!host?.tableId || typeof host.rowId !== "number" || host.rowId === lastRowId) return;
|
|
1058
|
+
lastRowId = host.rowId;
|
|
1059
|
+
try {
|
|
1060
|
+
const record = await fetchSelectedRecord(host.rowId, { keepEncoded: false });
|
|
1061
|
+
emit({ record, mappings: recordStream.peek()?.mappings ?? null });
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
console.error("[grist-widget-sdk] fetchSelectedRecord on a cursor move failed:", err);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
var recordsStream = new HostStream((emit) => {
|
|
1068
|
+
grist.onRecords((records, mappings) => emit({ records, mappings: asMappings(mappings) }), {
|
|
1069
|
+
keepEncoded: false,
|
|
1070
|
+
format: "rows"
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
var optionsStream = new HostStream((emit) => {
|
|
1074
|
+
grist.onOptions(
|
|
1075
|
+
(options, interaction) => emit({
|
|
1076
|
+
options: options ? { ...options } : null,
|
|
1077
|
+
interaction: normalizeWidgetInteraction(interaction ?? null)
|
|
1078
|
+
})
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
var newRecordStream = new HostStream((emit) => {
|
|
1082
|
+
if (typeof grist.onNewRecord !== "function") return;
|
|
1083
|
+
grist.onNewRecord((mappings) => emit({ mappings: asMappings(mappings) }));
|
|
1084
|
+
});
|
|
1085
|
+
var messageStream = new HostStream((emit) => {
|
|
1086
|
+
if (typeof grist.on !== "function") return;
|
|
1087
|
+
grist.on("message", (msg) => emit(msg));
|
|
1088
|
+
});
|
|
1089
|
+
var ALL_STREAMS = [recordStream, recordsStream, optionsStream, newRecordStream, messageStream];
|
|
1090
|
+
|
|
1091
|
+
// src/sdk/runtime/ready.ts
|
|
1092
|
+
var ACCESS_RANK2 = {
|
|
1093
|
+
none: 0,
|
|
1094
|
+
"read table": 1,
|
|
1095
|
+
full: 2
|
|
1096
|
+
};
|
|
1097
|
+
var inFlight2 = null;
|
|
1098
|
+
var lastAccess = null;
|
|
1099
|
+
var lastColumnNames = null;
|
|
1100
|
+
function columnNamesOf(columns) {
|
|
1101
|
+
return new Set((columns ?? []).map((c) => typeof c === "string" ? c : c.name));
|
|
1102
|
+
}
|
|
1103
|
+
function shouldReinit(next, nextColumns) {
|
|
1104
|
+
if (!lastAccess) return true;
|
|
1105
|
+
if (ACCESS_RANK2[next] > ACCESS_RANK2[lastAccess]) return true;
|
|
1106
|
+
for (const name of nextColumns) {
|
|
1107
|
+
if (!lastColumnNames?.has(name)) return true;
|
|
1108
|
+
}
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
function ensureGristReady(payload) {
|
|
1112
|
+
const access = payload.requiredAccess ?? "read table";
|
|
1113
|
+
const columns = columnNamesOf(payload.columns);
|
|
1114
|
+
if (inFlight2 && !shouldReinit(access, columns)) return inFlight2;
|
|
1115
|
+
lastAccess = access;
|
|
1116
|
+
lastColumnNames = columns;
|
|
1117
|
+
const next = Promise.resolve().then(() => {
|
|
1118
|
+
const result = grist.ready(payload);
|
|
1119
|
+
return result instanceof Promise ? result : Promise.resolve();
|
|
1120
|
+
});
|
|
1121
|
+
inFlight2 = next.catch((err) => {
|
|
1122
|
+
inFlight2 = null;
|
|
1123
|
+
lastAccess = null;
|
|
1124
|
+
lastColumnNames = null;
|
|
1125
|
+
throw err;
|
|
1126
|
+
});
|
|
1127
|
+
return inFlight2;
|
|
1128
|
+
}
|
|
1129
|
+
function resetGristReady() {
|
|
1130
|
+
inFlight2 = null;
|
|
1131
|
+
lastAccess = null;
|
|
1132
|
+
lastColumnNames = null;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/sdk/helpers/grist-theme-payload.ts
|
|
1136
|
+
function parseGristThemePayload(payload) {
|
|
1137
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1138
|
+
const raw = payload;
|
|
1139
|
+
const top = raw.theme;
|
|
1140
|
+
if (top === "light" || top === "dark") return top;
|
|
1141
|
+
if (top && typeof top === "object") {
|
|
1142
|
+
const appearance = top.appearance;
|
|
1143
|
+
if (appearance === "light" || appearance === "dark") return appearance;
|
|
1144
|
+
}
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// src/sdk/runtime/runtime.ts
|
|
1149
|
+
var READY_TIMEOUT_MS = 3e4;
|
|
1150
|
+
function initialRuntimeState(options = {}) {
|
|
1151
|
+
return {
|
|
1152
|
+
connection: "connecting",
|
|
1153
|
+
connectionError: null,
|
|
1154
|
+
declaredAccess: options.requiredAccess ?? "read table",
|
|
1155
|
+
grantedAccess: null,
|
|
1156
|
+
record: null,
|
|
1157
|
+
records: null,
|
|
1158
|
+
isNewRecord: false,
|
|
1159
|
+
mappings: null,
|
|
1160
|
+
widgetOptions: null,
|
|
1161
|
+
interaction: null,
|
|
1162
|
+
currentTableId: null,
|
|
1163
|
+
theme: null,
|
|
1164
|
+
actionStatus: "idle",
|
|
1165
|
+
actionError: null,
|
|
1166
|
+
readError: null,
|
|
1167
|
+
mapBackSkipped: []
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function isEmbedded() {
|
|
1171
|
+
try {
|
|
1172
|
+
return typeof window !== "undefined" && window.self !== window.top;
|
|
1173
|
+
} catch {
|
|
1174
|
+
return true;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
var hasGrist = () => typeof grist !== "undefined" && grist !== null;
|
|
1178
|
+
function createGristRuntime(options = {}) {
|
|
1179
|
+
let state = initialRuntimeState(options);
|
|
1180
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1181
|
+
let starts = 0;
|
|
1182
|
+
let stopStreams = null;
|
|
1183
|
+
let attempt = 0;
|
|
1184
|
+
const update = (patch) => {
|
|
1185
|
+
let changed = false;
|
|
1186
|
+
for (const key of Object.keys(patch)) {
|
|
1187
|
+
if (!Object.is(state[key], patch[key])) changed = true;
|
|
1188
|
+
}
|
|
1189
|
+
if (!changed) return;
|
|
1190
|
+
state = { ...state, ...patch };
|
|
1191
|
+
for (const l of [...listeners]) l();
|
|
1192
|
+
};
|
|
1193
|
+
const payload = {
|
|
1194
|
+
requiredAccess: options.requiredAccess ?? "read table",
|
|
1195
|
+
columns: options.columns ?? [],
|
|
1196
|
+
...options.allowSelectBy !== void 0 ? { allowSelectBy: options.allowSelectBy } : {},
|
|
1197
|
+
...options.hasCustomOptions !== void 0 ? { hasCustomOptions: options.hasCustomOptions } : {},
|
|
1198
|
+
...options.onEditOptions ? { onEditOptions: options.onEditOptions } : {}
|
|
1199
|
+
};
|
|
1200
|
+
const refreshTable = async () => {
|
|
1201
|
+
try {
|
|
1202
|
+
const tableId = await grist.selectedTable?.getTableId();
|
|
1203
|
+
if (tableId) update({ currentTableId: tableId });
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
const refreshMappings = async () => {
|
|
1208
|
+
if (typeof grist.sectionApi?.mappings !== "function") return;
|
|
1209
|
+
try {
|
|
1210
|
+
const mappings = await grist.sectionApi.mappings();
|
|
1211
|
+
if (state.mappings === null) update({ mappings: mappings ?? {} });
|
|
1212
|
+
} catch {
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
const connect = async () => {
|
|
1216
|
+
const mine = ++attempt;
|
|
1217
|
+
update({ connection: "connecting", connectionError: null });
|
|
1218
|
+
let timer;
|
|
1219
|
+
try {
|
|
1220
|
+
await Promise.race([
|
|
1221
|
+
ensureGristReady(payload),
|
|
1222
|
+
new Promise((_, reject) => {
|
|
1223
|
+
timer = setTimeout(() => reject(new Error(`Grist did not answer grist.ready() within ${READY_TIMEOUT_MS / 1e3} s.`)), READY_TIMEOUT_MS);
|
|
1224
|
+
})
|
|
1225
|
+
]);
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
if (mine === attempt) update({ connection: "error", connectionError: err instanceof Error ? err.message : String(err) });
|
|
1228
|
+
return;
|
|
1229
|
+
} finally {
|
|
1230
|
+
clearTimeout(timer);
|
|
1231
|
+
}
|
|
1232
|
+
if (mine !== attempt) return;
|
|
1233
|
+
update({ connection: "ready" });
|
|
1234
|
+
await Promise.all([refreshMappings(), refreshTable()]);
|
|
1235
|
+
};
|
|
1236
|
+
const listen = () => {
|
|
1237
|
+
const offs = [
|
|
1238
|
+
// Copies: a host may reuse one record object and mutate it in place.
|
|
1239
|
+
recordStream.subscribe(
|
|
1240
|
+
({ record, mappings }) => update({
|
|
1241
|
+
record: record ? { ...record } : null,
|
|
1242
|
+
isNewRecord: false,
|
|
1243
|
+
...mappings ? { mappings } : {}
|
|
1244
|
+
})
|
|
1245
|
+
),
|
|
1246
|
+
recordsStream.subscribe(({ records, mappings }) => {
|
|
1247
|
+
update({ records: records.map((r) => ({ ...r })), ...mappings ? { mappings } : {} });
|
|
1248
|
+
void refreshTable();
|
|
1249
|
+
}),
|
|
1250
|
+
optionsStream.subscribe(
|
|
1251
|
+
({ options: widgetOptions, interaction }) => update({
|
|
1252
|
+
widgetOptions,
|
|
1253
|
+
interaction,
|
|
1254
|
+
grantedAccess: parseAccess(interaction?.access_level) ?? state.grantedAccess
|
|
1255
|
+
})
|
|
1256
|
+
),
|
|
1257
|
+
newRecordStream.subscribe(({ mappings }) => update({ isNewRecord: true, ...mappings ? { mappings } : {} })),
|
|
1258
|
+
messageStream.subscribe((msg) => {
|
|
1259
|
+
const theme = parseGristThemePayload(msg);
|
|
1260
|
+
if (theme) update({ theme });
|
|
1261
|
+
})
|
|
1262
|
+
];
|
|
1263
|
+
return () => offs.forEach((off) => off());
|
|
1264
|
+
};
|
|
1265
|
+
const begin = () => {
|
|
1266
|
+
if (!isEmbedded()) return update({ connection: "not-embedded" });
|
|
1267
|
+
if (!hasGrist()) {
|
|
1268
|
+
if (typeof document !== "undefined" && document.readyState !== "complete" && typeof window !== "undefined") {
|
|
1269
|
+
window.addEventListener("load", () => hasGrist() ? begin() : update({ connection: "unavailable" }), { once: true });
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
return update({ connection: "unavailable" });
|
|
1273
|
+
}
|
|
1274
|
+
stopStreams ?? (stopStreams = listen());
|
|
1275
|
+
void connect();
|
|
1276
|
+
};
|
|
1277
|
+
return {
|
|
1278
|
+
options,
|
|
1279
|
+
getState: () => state,
|
|
1280
|
+
subscribe(listener) {
|
|
1281
|
+
listeners.add(listener);
|
|
1282
|
+
return () => listeners.delete(listener);
|
|
1283
|
+
},
|
|
1284
|
+
start() {
|
|
1285
|
+
if (starts++ === 0) begin();
|
|
1286
|
+
return () => {
|
|
1287
|
+
if (--starts > 0) return;
|
|
1288
|
+
stopStreams?.();
|
|
1289
|
+
stopStreams = null;
|
|
1290
|
+
};
|
|
1291
|
+
},
|
|
1292
|
+
async reload() {
|
|
1293
|
+
if (!hasGrist() || !isEmbedded()) return begin();
|
|
1294
|
+
resetGristReady();
|
|
1295
|
+
update({ mappings: null });
|
|
1296
|
+
stopStreams ?? (stopStreams = listen());
|
|
1297
|
+
await connect();
|
|
1298
|
+
},
|
|
1299
|
+
update
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/sdk/use-grist.ts
|
|
1304
|
+
var GristContext = createContext(null);
|
|
1305
|
+
GristContext.displayName = "GristContext";
|
|
1306
|
+
function useGristRuntime() {
|
|
1307
|
+
return useContext(GristContext);
|
|
1308
|
+
}
|
|
1309
|
+
function newConnection(options) {
|
|
1310
|
+
const runtime = createGristRuntime(options);
|
|
1311
|
+
return { runtime, api: createGristApi(runtime) };
|
|
1312
|
+
}
|
|
1313
|
+
var pageConnection = null;
|
|
1314
|
+
function resetGristForTests() {
|
|
1315
|
+
pageConnection = null;
|
|
1316
|
+
resetGristReady();
|
|
1317
|
+
for (const s of ALL_STREAMS) s.reset();
|
|
1318
|
+
}
|
|
1319
|
+
var GristContextProvider = GristContext.Provider;
|
|
1320
|
+
var serverState = () => initialRuntimeState();
|
|
1321
|
+
function useGristState(connection) {
|
|
1322
|
+
const { runtime } = connection;
|
|
1323
|
+
useEffect(() => runtime.start(), [runtime]);
|
|
1324
|
+
return useSyncExternalStore(runtime.subscribe, runtime.getState, serverState);
|
|
1325
|
+
}
|
|
1326
|
+
var NO_ISSUES = Object.freeze([]);
|
|
1327
|
+
function useMapped(state, options) {
|
|
1328
|
+
const { record, records, mappings } = state;
|
|
1329
|
+
const { columns, schema } = options;
|
|
1330
|
+
return useMemo(() => {
|
|
1331
|
+
const issues = [];
|
|
1332
|
+
const rows = records && (columns?.length ? mappings !== null : true) ? records.map((r) => {
|
|
1333
|
+
const out = mapAndCheck(r, columns, mappings, schema);
|
|
1334
|
+
issues.push(...out.issues);
|
|
1335
|
+
return out.value;
|
|
1336
|
+
}) : null;
|
|
1337
|
+
const selected = mapAndCheck(record, columns, mappings, schema);
|
|
1338
|
+
if (record && !records?.some((r) => r.id === record.id)) issues.push(...selected.issues);
|
|
1339
|
+
const mappedRecords = rows && rows.every((r) => r !== null) ? rows : null;
|
|
1340
|
+
return { record: selected.value, records: mappedRecords, issues: issues.length ? issues : NO_ISSUES };
|
|
1341
|
+
}, [record, records, mappings, columns, schema]);
|
|
1342
|
+
}
|
|
1343
|
+
function buildResult(state, mapped, connection, mapBack, resolveMappedColumnId) {
|
|
1344
|
+
const { runtime, api } = connection;
|
|
1345
|
+
const status = deriveStatus(state);
|
|
1346
|
+
const columns = runtime.options.columns;
|
|
1347
|
+
return {
|
|
1348
|
+
...api,
|
|
1349
|
+
status,
|
|
1350
|
+
isAvailable: state.connection === "connecting" ? null : state.connection !== "not-embedded" && state.connection !== "unavailable",
|
|
1351
|
+
isReady: status === "ready",
|
|
1352
|
+
error: deriveError(state),
|
|
1353
|
+
theme: state.theme,
|
|
1354
|
+
record: state.record,
|
|
1355
|
+
records: state.records,
|
|
1356
|
+
mappedRecord: mapped.record,
|
|
1357
|
+
mappedRecords: mapped.records,
|
|
1358
|
+
cellIssues: mapped.issues,
|
|
1359
|
+
mappings: state.mappings ?? EMPTY_MAPPINGS,
|
|
1360
|
+
columnMappingStatus: deriveColumnMappingStatus(columns, state.mappings),
|
|
1361
|
+
mode: state.isNewRecord ? "new-row" : state.record ? "row" : "empty",
|
|
1362
|
+
resolveMappedColumnId,
|
|
1363
|
+
mapBack,
|
|
1364
|
+
currentTableId: state.currentTableId,
|
|
1365
|
+
actionStatus: state.actionStatus,
|
|
1366
|
+
actionError: state.actionError,
|
|
1367
|
+
widgetOptions: state.widgetOptions
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
var EMPTY_MAPPINGS = Object.freeze({});
|
|
1371
|
+
function useGristResult(connection) {
|
|
1372
|
+
const state = useGristState(connection);
|
|
1373
|
+
const mappings = state.mappings ?? EMPTY_MAPPINGS;
|
|
1374
|
+
const { runtime } = connection;
|
|
1375
|
+
const resolveMappedColumnId = useCallback(
|
|
1376
|
+
(name) => {
|
|
1377
|
+
const real = mappings[name];
|
|
1378
|
+
if (typeof real === "string" && real) return real;
|
|
1379
|
+
if (Array.isArray(real) && typeof real[0] === "string") return real[0];
|
|
1380
|
+
return name;
|
|
1381
|
+
},
|
|
1382
|
+
[mappings]
|
|
1383
|
+
);
|
|
1384
|
+
const mapBack = useCallback(
|
|
1385
|
+
(patch) => {
|
|
1386
|
+
const { fields, skipped } = mapBackPatch(patch, mappings);
|
|
1387
|
+
const previous = runtime.getState().mapBackSkipped;
|
|
1388
|
+
if (previous.join("\0") !== skipped.join("\0")) runtime.update({ mapBackSkipped: skipped });
|
|
1389
|
+
return fields;
|
|
1390
|
+
},
|
|
1391
|
+
[mappings, runtime]
|
|
1392
|
+
);
|
|
1393
|
+
const mapped = useMapped(state, runtime.options);
|
|
1394
|
+
return useMemo(
|
|
1395
|
+
() => buildResult(state, mapped, connection, mapBack, resolveMappedColumnId),
|
|
1396
|
+
[state, mapped, connection, mapBack, resolveMappedColumnId]
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
function useGrist(options) {
|
|
1400
|
+
const fromProvider = useContext(GristContext);
|
|
1401
|
+
const connection = fromProvider ?? (pageConnection ?? (pageConnection = newConnection(options)));
|
|
1402
|
+
return useGristResult(connection);
|
|
1403
|
+
}
|
|
1404
|
+
function useGristOptional() {
|
|
1405
|
+
const fromProvider = useContext(GristContext);
|
|
1406
|
+
const result = useGristResult(fromProvider ?? DETACHED);
|
|
1407
|
+
return fromProvider ? result : null;
|
|
1408
|
+
}
|
|
1409
|
+
function useGristContext() {
|
|
1410
|
+
if (!useContext(GristContext)) {
|
|
1411
|
+
throw new Error("useGristContext() must be used inside <GristWidgetProvider> (mountGristWidget renders one).");
|
|
1412
|
+
}
|
|
1413
|
+
return useGrist();
|
|
1414
|
+
}
|
|
1415
|
+
var DETACHED = (() => {
|
|
1416
|
+
const state = initialRuntimeState();
|
|
1417
|
+
const runtime = {
|
|
1418
|
+
options: {},
|
|
1419
|
+
getState: () => state,
|
|
1420
|
+
subscribe: () => () => {
|
|
1421
|
+
},
|
|
1422
|
+
start: () => () => {
|
|
1423
|
+
},
|
|
1424
|
+
reload: async () => {
|
|
1425
|
+
},
|
|
1426
|
+
update: () => {
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
return { runtime, api: createGristApi(runtime) };
|
|
1430
|
+
})();
|
|
1431
|
+
function useNearestGristState() {
|
|
1432
|
+
const connection = useContext(GristContext);
|
|
1433
|
+
const state = useGristState(connection ?? DETACHED);
|
|
1434
|
+
const { issues } = useMapped(state, (connection ?? DETACHED).runtime.options);
|
|
1435
|
+
return { connection, state, cellIssues: issues };
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// src/sdk/components/grist-widget-provider.tsx
|
|
1439
|
+
import { useState } from "react";
|
|
1440
|
+
import { jsx } from "react/jsx-runtime";
|
|
1441
|
+
function GristWidgetProvider({ options, children }) {
|
|
1442
|
+
const [connection] = useState(() => newConnection(options));
|
|
1443
|
+
return /* @__PURE__ */ jsx(GristContextProvider, { value: connection, children });
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// src/sdk/components/grist-boundary.tsx
|
|
1447
|
+
import { useCallback as useCallback2, useState as useState2 } from "react";
|
|
1448
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
1449
|
+
var shellStyle = {
|
|
1450
|
+
alignItems: "center",
|
|
1451
|
+
background: "var(--background)",
|
|
1452
|
+
boxSizing: "border-box",
|
|
1453
|
+
color: "var(--foreground)",
|
|
1454
|
+
display: "flex",
|
|
1455
|
+
flexDirection: "column",
|
|
1456
|
+
justifyContent: "center",
|
|
1457
|
+
minHeight: "100%",
|
|
1458
|
+
padding: "clamp(16px, 5vh, 32px)",
|
|
1459
|
+
width: "100%"
|
|
1460
|
+
};
|
|
1461
|
+
var cardStyle = {
|
|
1462
|
+
background: "var(--card)",
|
|
1463
|
+
border: "1px solid var(--border)",
|
|
1464
|
+
borderRadius: 12,
|
|
1465
|
+
boxShadow: "0 1px 3px rgb(0 0 0 / 0.12)",
|
|
1466
|
+
maxWidth: 420,
|
|
1467
|
+
padding: "20px 22px",
|
|
1468
|
+
textAlign: "center",
|
|
1469
|
+
width: "100%"
|
|
1470
|
+
};
|
|
1471
|
+
var titleStyle = {
|
|
1472
|
+
color: "var(--foreground)",
|
|
1473
|
+
fontSize: 15,
|
|
1474
|
+
fontWeight: 600,
|
|
1475
|
+
lineHeight: 1.35,
|
|
1476
|
+
margin: 0
|
|
1477
|
+
};
|
|
1478
|
+
var messageStyle = {
|
|
1479
|
+
color: "var(--muted-foreground)",
|
|
1480
|
+
fontSize: 14,
|
|
1481
|
+
lineHeight: 1.5,
|
|
1482
|
+
margin: "10px 0 0"
|
|
1483
|
+
};
|
|
1484
|
+
var buttonStyle = {
|
|
1485
|
+
background: "var(--background)",
|
|
1486
|
+
border: "1px solid var(--border)",
|
|
1487
|
+
borderRadius: 8,
|
|
1488
|
+
color: "var(--foreground)",
|
|
1489
|
+
cursor: "pointer",
|
|
1490
|
+
fontSize: 13,
|
|
1491
|
+
fontWeight: 500,
|
|
1492
|
+
marginTop: 16,
|
|
1493
|
+
padding: "8px 14px"
|
|
1494
|
+
};
|
|
1495
|
+
function GristBoundaryScreen({
|
|
1496
|
+
title,
|
|
1497
|
+
message: message2,
|
|
1498
|
+
tone = "default",
|
|
1499
|
+
showRetry = false,
|
|
1500
|
+
actions,
|
|
1501
|
+
retryLabel = "Try reconnecting",
|
|
1502
|
+
retryBusyLabel = "Reconnecting\u2026"
|
|
1503
|
+
}) {
|
|
1504
|
+
return /* @__PURE__ */ jsx2("div", { style: shellStyle, role: tone === "error" ? "alert" : "status", children: /* @__PURE__ */ jsxs("div", { style: cardStyle, children: [
|
|
1505
|
+
/* @__PURE__ */ jsx2("h2", { style: titleStyle, children: title }),
|
|
1506
|
+
/* @__PURE__ */ jsx2("p", { style: messageStyle, children: message2 }),
|
|
1507
|
+
showRetry && actions ? /* @__PURE__ */ jsx2(
|
|
1508
|
+
"button",
|
|
1509
|
+
{
|
|
1510
|
+
disabled: actions.isReloading,
|
|
1511
|
+
onClick: () => void actions.reload(),
|
|
1512
|
+
style: {
|
|
1513
|
+
...buttonStyle,
|
|
1514
|
+
cursor: actions.isReloading ? "not-allowed" : "pointer",
|
|
1515
|
+
opacity: actions.isReloading ? 0.7 : 1
|
|
1516
|
+
},
|
|
1517
|
+
type: "button",
|
|
1518
|
+
children: actions.isReloading ? retryBusyLabel : retryLabel
|
|
1519
|
+
}
|
|
1520
|
+
) : null
|
|
1521
|
+
] }) });
|
|
1522
|
+
}
|
|
1523
|
+
function boundaryScreen(state, columns) {
|
|
1524
|
+
switch (state.connection) {
|
|
1525
|
+
case "connecting":
|
|
1526
|
+
return { title: "Connecting to Grist", message: "Asking Grist for the access this widget needs\u2026" };
|
|
1527
|
+
case "not-embedded":
|
|
1528
|
+
return {
|
|
1529
|
+
title: "Open inside Grist",
|
|
1530
|
+
message: "Open this page as a custom widget inside a Grist document (it runs in an embedded panel, not on its own).",
|
|
1531
|
+
retry: true
|
|
1532
|
+
};
|
|
1533
|
+
case "unavailable":
|
|
1534
|
+
return {
|
|
1535
|
+
title: "Open inside Grist",
|
|
1536
|
+
message: "The Grist plugin did not load. Reload the page, or open this URL as a custom widget inside a Grist document.",
|
|
1537
|
+
retry: true
|
|
1538
|
+
};
|
|
1539
|
+
case "error":
|
|
1540
|
+
return {
|
|
1541
|
+
title: "Something went wrong",
|
|
1542
|
+
message: state.connectionError ?? "Something went wrong connecting to Grist. Try again, or reload the page.",
|
|
1543
|
+
tone: "error",
|
|
1544
|
+
retry: true
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
if (accessDenied(state)) {
|
|
1548
|
+
return {
|
|
1549
|
+
title: "More access needed",
|
|
1550
|
+
message: `Set Access level to at least "${accessLabel(state.declaredAccess)}" in the Custom widget settings.`,
|
|
1551
|
+
tone: "error",
|
|
1552
|
+
retry: true
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
const mapping = deriveColumnMappingStatus(columns, state.mappings);
|
|
1556
|
+
if (mapping.pending) return { title: "Connecting to Grist", message: "Waiting for Grist to report the column mapping\u2026" };
|
|
1557
|
+
if (!mapping.ok) {
|
|
1558
|
+
return {
|
|
1559
|
+
title: "Map your columns",
|
|
1560
|
+
message: "Open the widget settings in Grist (Creator Panel \u2192 Custom widget) and connect each required column to your table."
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
function GristBoundary({ children }) {
|
|
1566
|
+
const connection = useGristRuntime();
|
|
1567
|
+
if (!connection) throw new Error("<GristBoundary> must be used inside <GristWidgetProvider> (mountGristWidget renders both).");
|
|
1568
|
+
const state = useGristState(connection);
|
|
1569
|
+
const [isReloading, setIsReloading] = useState2(false);
|
|
1570
|
+
const reload = useCallback2(async () => {
|
|
1571
|
+
setIsReloading(true);
|
|
1572
|
+
try {
|
|
1573
|
+
await connection.runtime.reload();
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
console.error("[grist-widget-sdk] reload failed:", err);
|
|
1576
|
+
} finally {
|
|
1577
|
+
setIsReloading(false);
|
|
1578
|
+
}
|
|
1579
|
+
}, [connection]);
|
|
1580
|
+
const screen = boundaryScreen(state, connection.runtime.options.columns);
|
|
1581
|
+
if (!screen) return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
1582
|
+
return /* @__PURE__ */ jsx2(
|
|
1583
|
+
GristBoundaryScreen,
|
|
1584
|
+
{
|
|
1585
|
+
title: screen.title,
|
|
1586
|
+
message: screen.message,
|
|
1587
|
+
tone: screen.tone,
|
|
1588
|
+
showRetry: screen.retry,
|
|
1589
|
+
actions: { reload, isReloading },
|
|
1590
|
+
retryLabel: screen.tone === "error" ? "Try again" : "Try reconnecting",
|
|
1591
|
+
retryBusyLabel: screen.tone === "error" ? "Retrying\u2026" : "Reconnecting\u2026"
|
|
1592
|
+
}
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// src/sdk/use-grist-schema.ts
|
|
1597
|
+
import { useCallback as useCallback3, useEffect as useEffect2, useMemo as useMemo2, useState as useState3 } from "react";
|
|
1598
|
+
function filterReplicaDocumentColumns(document2, options, extraExcludeColumn) {
|
|
1599
|
+
return {
|
|
1600
|
+
...document2,
|
|
1601
|
+
tables: Object.fromEntries(
|
|
1602
|
+
Object.entries(document2.tables).map(([tableId, table]) => [
|
|
1603
|
+
tableId,
|
|
1604
|
+
{
|
|
1605
|
+
...table,
|
|
1606
|
+
columns: Object.fromEntries(
|
|
1607
|
+
Object.entries(table.columns ?? {}).filter(([colId, col]) => {
|
|
1608
|
+
if (isGristNoiseColumn(colId, col, options)) return false;
|
|
1609
|
+
if (extraExcludeColumn?.({ tableId, colId, col })) return false;
|
|
1610
|
+
return true;
|
|
1611
|
+
})
|
|
1612
|
+
)
|
|
1613
|
+
}
|
|
1614
|
+
])
|
|
1615
|
+
)
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
function useGristSchema(options) {
|
|
1619
|
+
const requiredAccess = options?.requiredAccess ?? "read table";
|
|
1620
|
+
const requestedTableId = options?.tableId;
|
|
1621
|
+
const excludeSystemTableData = options?.excludeSystemTableData ?? true;
|
|
1622
|
+
const replicaRowMode = options?.replicaRowMode ?? "schema+samples";
|
|
1623
|
+
const sampleRowLimit = options?.sampleRowLimit ?? 10;
|
|
1624
|
+
const replicaSource = options?.source;
|
|
1625
|
+
const excludeId = options?.excludeId ?? true;
|
|
1626
|
+
const excludeGristHelpers = options?.excludeGristHelpers ?? true;
|
|
1627
|
+
const excludeLoggingColumns = options?.excludeLoggingColumns ?? true;
|
|
1628
|
+
const excludeManualSort = options?.excludeManualSort ?? false;
|
|
1629
|
+
const extraExcludeColumn = options?.extraExcludeColumn;
|
|
1630
|
+
const w = useGrist({
|
|
1631
|
+
requiredAccess,
|
|
1632
|
+
columns: options?.columns,
|
|
1633
|
+
allowSelectBy: options?.allowSelectBy,
|
|
1634
|
+
hasCustomOptions: options?.hasCustomOptions,
|
|
1635
|
+
onEditOptions: options?.onEditOptions
|
|
1636
|
+
});
|
|
1637
|
+
const connection = useGristRuntime();
|
|
1638
|
+
const isAvailable = w.isAvailable;
|
|
1639
|
+
const readyError = w.status === "error" || w.status === "unavailable" ? w.error : null;
|
|
1640
|
+
const docApi = w.isReady && typeof grist !== "undefined" ? grist.docApi ?? null : null;
|
|
1641
|
+
const [document2, setDocument] = useState3(null);
|
|
1642
|
+
const [loading, setLoading] = useState3(false);
|
|
1643
|
+
const [error, setError] = useState3(null);
|
|
1644
|
+
const includeRowsForBuild = replicaRowMode === "schema-only" ? false : replicaRowMode === "schema+data" ? "full" : "samples";
|
|
1645
|
+
const load = useCallback3(async () => {
|
|
1646
|
+
if (!docApi) return;
|
|
1647
|
+
setLoading(true);
|
|
1648
|
+
setError(null);
|
|
1649
|
+
try {
|
|
1650
|
+
const buildOpts = {
|
|
1651
|
+
includeSystemTableData: !excludeSystemTableData,
|
|
1652
|
+
includeRows: includeRowsForBuild,
|
|
1653
|
+
sampleRowLimit,
|
|
1654
|
+
source: replicaSource
|
|
1655
|
+
};
|
|
1656
|
+
const raw = await buildReplicaDocumentFromDocApi(docApi, buildOpts);
|
|
1657
|
+
const filtered = filterReplicaDocumentColumns(
|
|
1658
|
+
raw,
|
|
1659
|
+
{
|
|
1660
|
+
excludeId,
|
|
1661
|
+
excludeGristHelpers,
|
|
1662
|
+
excludeLoggingColumns,
|
|
1663
|
+
excludeManualSort
|
|
1664
|
+
},
|
|
1665
|
+
extraExcludeColumn
|
|
1666
|
+
);
|
|
1667
|
+
setDocument(filtered);
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
1670
|
+
setDocument(null);
|
|
1671
|
+
setError(message2);
|
|
1672
|
+
if (isAccessInsufficientError(message2)) connection?.runtime.update({ readError: message2 });
|
|
1673
|
+
} finally {
|
|
1674
|
+
setLoading(false);
|
|
1675
|
+
}
|
|
1676
|
+
}, [
|
|
1677
|
+
docApi,
|
|
1678
|
+
connection,
|
|
1679
|
+
excludeSystemTableData,
|
|
1680
|
+
replicaRowMode,
|
|
1681
|
+
sampleRowLimit,
|
|
1682
|
+
excludeId,
|
|
1683
|
+
excludeGristHelpers,
|
|
1684
|
+
excludeLoggingColumns,
|
|
1685
|
+
excludeManualSort,
|
|
1686
|
+
extraExcludeColumn,
|
|
1687
|
+
replicaSource
|
|
1688
|
+
]);
|
|
1689
|
+
useEffect2(() => {
|
|
1690
|
+
if (docApi) void load();
|
|
1691
|
+
}, [docApi, load]);
|
|
1692
|
+
const buildDocumentJson = useCallback3((target, options2) => {
|
|
1693
|
+
const includeRows = options2?.includeRows ?? "full";
|
|
1694
|
+
const space = options2?.space ?? 2;
|
|
1695
|
+
const payload = buildReplicaJsonPayload(target, {
|
|
1696
|
+
includeRows,
|
|
1697
|
+
sampleRowLimit: options2?.sampleRowLimit ?? sampleRowLimit
|
|
1698
|
+
});
|
|
1699
|
+
return JSON.stringify(payload, null, space);
|
|
1700
|
+
}, [sampleRowLimit]);
|
|
1701
|
+
const getDocumentJson = useCallback3(
|
|
1702
|
+
(options2) => {
|
|
1703
|
+
if (!document2) return null;
|
|
1704
|
+
return buildDocumentJson(document2, options2);
|
|
1705
|
+
},
|
|
1706
|
+
[document2, buildDocumentJson]
|
|
1707
|
+
);
|
|
1708
|
+
const table = useMemo2(() => {
|
|
1709
|
+
if (!document2 || !requestedTableId) return null;
|
|
1710
|
+
const slice = document2.tables[requestedTableId];
|
|
1711
|
+
if (!slice) return null;
|
|
1712
|
+
return { tableId: requestedTableId, ...slice };
|
|
1713
|
+
}, [document2, requestedTableId]);
|
|
1714
|
+
const buildTableJson = useCallback3(
|
|
1715
|
+
(target, options2) => {
|
|
1716
|
+
const includeRows = options2?.includeRows ?? "full";
|
|
1717
|
+
const limit = options2?.sampleRowLimit ?? sampleRowLimit;
|
|
1718
|
+
const space = options2?.space ?? 2;
|
|
1719
|
+
const { tableId, ...rest } = target;
|
|
1720
|
+
let body = { ...rest };
|
|
1721
|
+
if (includeRows === "none") {
|
|
1722
|
+
const { rows: _r, ...r } = body;
|
|
1723
|
+
body = r;
|
|
1724
|
+
} else if (includeRows === "samples") {
|
|
1725
|
+
const r = body.rows ?? [];
|
|
1726
|
+
body = { ...body, rows: r.slice(0, limit) };
|
|
1727
|
+
}
|
|
1728
|
+
return JSON.stringify({ tableId, ...body }, null, space);
|
|
1729
|
+
},
|
|
1730
|
+
[sampleRowLimit]
|
|
1731
|
+
);
|
|
1732
|
+
const getTableJson = useCallback3(
|
|
1733
|
+
(options2) => {
|
|
1734
|
+
if (!table) return null;
|
|
1735
|
+
return buildTableJson(table, options2);
|
|
1736
|
+
},
|
|
1737
|
+
[table, buildTableJson]
|
|
1738
|
+
);
|
|
1739
|
+
const noiseColumnMatcher = useCallback3(
|
|
1740
|
+
(colId, col) => isGristNoiseColumn(colId, col, {
|
|
1741
|
+
excludeId,
|
|
1742
|
+
excludeGristHelpers,
|
|
1743
|
+
excludeLoggingColumns,
|
|
1744
|
+
excludeManualSort
|
|
1745
|
+
}),
|
|
1746
|
+
[excludeId, excludeGristHelpers, excludeLoggingColumns, excludeManualSort]
|
|
1747
|
+
);
|
|
1748
|
+
const sortColumns = useCallback3((colIds, columns) => {
|
|
1749
|
+
return [...colIds].sort((a, b) => {
|
|
1750
|
+
const la = (columns[a]?.label || a).toLowerCase();
|
|
1751
|
+
const lb = (columns[b]?.label || b).toLowerCase();
|
|
1752
|
+
if (la !== lb) return la.localeCompare(lb);
|
|
1753
|
+
return a.localeCompare(b);
|
|
1754
|
+
});
|
|
1755
|
+
}, []);
|
|
1756
|
+
return {
|
|
1757
|
+
isAvailable,
|
|
1758
|
+
document: document2,
|
|
1759
|
+
replicaDocument: document2,
|
|
1760
|
+
table,
|
|
1761
|
+
documentJson: document2 ? buildDocumentJson(document2) : null,
|
|
1762
|
+
getDocumentJson,
|
|
1763
|
+
buildDocumentJson,
|
|
1764
|
+
tableJson: table ? buildTableJson(table) : null,
|
|
1765
|
+
getTableJson,
|
|
1766
|
+
buildTableJson,
|
|
1767
|
+
isNoiseColumnId: noiseColumnMatcher,
|
|
1768
|
+
sortColumnIds: sortColumns,
|
|
1769
|
+
loading,
|
|
1770
|
+
error: readyError ?? error,
|
|
1771
|
+
reload: load
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// src/sdk/use-widget-metadata.ts
|
|
1776
|
+
import { useEffect as useEffect3 } from "react";
|
|
1777
|
+
function useWidgetMetadata(input) {
|
|
1778
|
+
useEffect3(() => {
|
|
1779
|
+
if (!input) return;
|
|
1780
|
+
const { title, description } = input;
|
|
1781
|
+
const canonicalUrl = window.location.href;
|
|
1782
|
+
document.title = title;
|
|
1783
|
+
setMetaTag("name", "title", title);
|
|
1784
|
+
setMetaTag("name", "description", description);
|
|
1785
|
+
setMetaTag("property", "og:title", title);
|
|
1786
|
+
setMetaTag("property", "og:description", description);
|
|
1787
|
+
setMetaTag("property", "og:type", "website");
|
|
1788
|
+
setMetaTag("property", "og:url", canonicalUrl);
|
|
1789
|
+
setMetaTag("name", "twitter:card", "summary");
|
|
1790
|
+
setMetaTag("name", "twitter:title", title);
|
|
1791
|
+
setMetaTag("name", "twitter:description", description);
|
|
1792
|
+
}, [input]);
|
|
1793
|
+
}
|
|
1794
|
+
function setMetaTag(key, value, content) {
|
|
1795
|
+
let meta = document.head.querySelector(
|
|
1796
|
+
`meta[${key}="${value}"]`
|
|
1797
|
+
);
|
|
1798
|
+
if (!meta) {
|
|
1799
|
+
meta = document.createElement("meta");
|
|
1800
|
+
meta.setAttribute(key, value);
|
|
1801
|
+
document.head.appendChild(meta);
|
|
1802
|
+
}
|
|
1803
|
+
meta.setAttribute("content", content);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
export {
|
|
1807
|
+
tableDataToRows,
|
|
1808
|
+
resolveGristFormulaKind,
|
|
1809
|
+
normalizeGristChoiceListEntries,
|
|
1810
|
+
extractGristAttachmentIdsFromCell,
|
|
1811
|
+
extractGristAttachmentId,
|
|
1812
|
+
gristAttachmentCellValues,
|
|
1813
|
+
gristAttachmentCellValue,
|
|
1814
|
+
mergeGristAttachmentCellValue,
|
|
1815
|
+
isAccessInsufficientError,
|
|
1816
|
+
deriveStatus,
|
|
1817
|
+
deriveColumnMappingStatus,
|
|
1818
|
+
checkGristCell,
|
|
1819
|
+
parseGristWidgetLinkingInfo,
|
|
1820
|
+
resetGristForTests,
|
|
1821
|
+
useGrist,
|
|
1822
|
+
useGristOptional,
|
|
1823
|
+
useGristContext,
|
|
1824
|
+
useNearestGristState,
|
|
1825
|
+
GristWidgetProvider,
|
|
1826
|
+
GristBoundary,
|
|
1827
|
+
useGristSchema,
|
|
1828
|
+
useWidgetMetadata
|
|
1829
|
+
};
|
|
1830
|
+
//# sourceMappingURL=chunk-O7NY46QY.js.map
|