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
|
@@ -20,23 +20,833 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/emulator/testing/index.ts
|
|
21
21
|
var testing_exports = {};
|
|
22
22
|
__export(testing_exports, {
|
|
23
|
-
act: () =>
|
|
23
|
+
act: () => import_react11.act,
|
|
24
24
|
actionsOf: () => actionsOf,
|
|
25
|
-
cleanup: () =>
|
|
25
|
+
cleanup: () => import_react11.cleanup,
|
|
26
26
|
eventsOf: () => eventsOf,
|
|
27
|
-
fireEvent: () =>
|
|
27
|
+
fireEvent: () => import_react11.fireEvent,
|
|
28
|
+
installJsdomStubs: () => installJsdomStubs,
|
|
28
29
|
presets: () => presets,
|
|
29
|
-
render: () =>
|
|
30
|
+
render: () => import_react11.render,
|
|
30
31
|
renderWithGrist: () => renderWithGrist,
|
|
31
|
-
screen: () =>
|
|
32
|
-
|
|
32
|
+
screen: () => import_react11.screen,
|
|
33
|
+
smokeTestWidget: () => smokeTestWidget,
|
|
34
|
+
waitFor: () => import_react11.waitFor,
|
|
33
35
|
waitForEvent: () => waitForEvent,
|
|
34
|
-
within: () =>
|
|
36
|
+
within: () => import_react11.within
|
|
35
37
|
});
|
|
36
38
|
module.exports = __toCommonJS(testing_exports);
|
|
37
39
|
|
|
38
40
|
// src/emulator/testing/render-with-grist.tsx
|
|
39
|
-
var
|
|
41
|
+
var import_react2 = require("@testing-library/react");
|
|
42
|
+
|
|
43
|
+
// src/sdk/use-grist.ts
|
|
44
|
+
var import_react = require("react");
|
|
45
|
+
|
|
46
|
+
// src/sdk/helpers/grist-value-codec.ts
|
|
47
|
+
var MARSHALLED_TAGS = /* @__PURE__ */ new Set(["E", "L", "D", "d", "R", "r", "P"]);
|
|
48
|
+
function isMarshalledTuple(value) {
|
|
49
|
+
return Array.isArray(value) && typeof value[0] === "string" && value[0].length <= 2 && MARSHALLED_TAGS.has(value[0]);
|
|
50
|
+
}
|
|
51
|
+
function coerceGristDocApiRefRowId(value) {
|
|
52
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
53
|
+
if (!Array.isArray(value) || value.length < 2) return void 0;
|
|
54
|
+
const tag = value[0];
|
|
55
|
+
if (tag === "R" && typeof value[2] === "number" && Number.isFinite(value[2])) return value[2];
|
|
56
|
+
if (tag === "r" && typeof value[1] === "number" && Number.isFinite(value[1])) return value[1];
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
function looksLikeAttachmentColumn(column) {
|
|
60
|
+
return column?.type === "Attachments";
|
|
61
|
+
}
|
|
62
|
+
function looksLikeChoiceListColumn(column) {
|
|
63
|
+
return column?.type === "ChoiceList";
|
|
64
|
+
}
|
|
65
|
+
function looksLikeRefListColumn(column) {
|
|
66
|
+
return typeof column?.type === "string" && column.type.startsWith("RefList:");
|
|
67
|
+
}
|
|
68
|
+
function looksLikeRefColumn(column) {
|
|
69
|
+
return typeof column?.type === "string" && column.type.startsWith("Ref:");
|
|
70
|
+
}
|
|
71
|
+
function looksLikeDateColumn(column) {
|
|
72
|
+
return column?.type === "Date";
|
|
73
|
+
}
|
|
74
|
+
function looksLikeDateTimeColumn(column) {
|
|
75
|
+
return typeof column?.type === "string" && column.type.startsWith("DateTime");
|
|
76
|
+
}
|
|
77
|
+
function decodeGristValue(value, column) {
|
|
78
|
+
if (value === null || value === void 0) return null;
|
|
79
|
+
if (isMarshalledTuple(value)) {
|
|
80
|
+
const tag = value[0];
|
|
81
|
+
switch (tag) {
|
|
82
|
+
case "E":
|
|
83
|
+
return {
|
|
84
|
+
__error: true,
|
|
85
|
+
type: typeof value[1] === "string" ? value[1] : "Error",
|
|
86
|
+
message: typeof value[2] === "string" ? value[2] : void 0,
|
|
87
|
+
traceback: typeof value[3] === "string" ? value[3] : void 0
|
|
88
|
+
};
|
|
89
|
+
case "L":
|
|
90
|
+
return value.slice(1).map((v) => decodeGristValue(v, void 0));
|
|
91
|
+
case "D":
|
|
92
|
+
if (typeof value[1] === "number") return new Date(value[1] * 1e3);
|
|
93
|
+
return null;
|
|
94
|
+
case "d":
|
|
95
|
+
if (typeof value[1] === "number") return new Date(value[1] * 1e3);
|
|
96
|
+
return null;
|
|
97
|
+
case "R":
|
|
98
|
+
return {
|
|
99
|
+
__ref: true,
|
|
100
|
+
tableId: typeof value[1] === "string" ? value[1] : void 0,
|
|
101
|
+
rowId: typeof value[2] === "number" ? value[2] : 0
|
|
102
|
+
};
|
|
103
|
+
case "r":
|
|
104
|
+
return { __ref: true, rowId: typeof value[1] === "number" ? value[1] : 0 };
|
|
105
|
+
case "P":
|
|
106
|
+
return null;
|
|
107
|
+
default:
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (looksLikeDateColumn(column) || looksLikeDateTimeColumn(column)) {
|
|
112
|
+
if (typeof value === "number" && Number.isFinite(value)) return new Date(value * 1e3);
|
|
113
|
+
}
|
|
114
|
+
if (looksLikeRefColumn(column) && typeof value === "number") {
|
|
115
|
+
return { __ref: true, rowId: value };
|
|
116
|
+
}
|
|
117
|
+
if ((looksLikeRefListColumn(column) || looksLikeChoiceListColumn(column) || looksLikeAttachmentColumn(column)) && Array.isArray(value)) {
|
|
118
|
+
return value.map((v) => decodeGristValue(v, void 0));
|
|
119
|
+
}
|
|
120
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
121
|
+
if (Array.isArray(value)) return value.map((v) => decodeGristValue(v, void 0));
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/sdk/helpers/grist-table-data.ts
|
|
126
|
+
function computeRowCount(data) {
|
|
127
|
+
const idCol = data.id;
|
|
128
|
+
if (Array.isArray(idCol)) return idCol.length;
|
|
129
|
+
for (const key of Object.keys(data)) {
|
|
130
|
+
const col = data[key];
|
|
131
|
+
if (Array.isArray(col)) return col.length;
|
|
132
|
+
}
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
function decodeColumnarToReplicaRows(data, columnIds, columns, maxRows) {
|
|
136
|
+
const nFull = computeRowCount(data);
|
|
137
|
+
const n = maxRows !== void 0 ? Math.min(nFull, maxRows) : nFull;
|
|
138
|
+
const out = [];
|
|
139
|
+
for (let i = 0; i < n; i++) {
|
|
140
|
+
const row = {};
|
|
141
|
+
for (const k of columnIds) {
|
|
142
|
+
const arr = data[k];
|
|
143
|
+
const raw = Array.isArray(arr) ? arr[i] : void 0;
|
|
144
|
+
if (k === "id") {
|
|
145
|
+
row[k] = raw;
|
|
146
|
+
} else {
|
|
147
|
+
row[k] = decodeGristValue(raw, columns[k]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
out.push(row);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function tableDataToRows(data) {
|
|
155
|
+
const idCol = data.id;
|
|
156
|
+
if (!Array.isArray(idCol) || idCol.length === 0) return [];
|
|
157
|
+
const keys = Object.keys(data);
|
|
158
|
+
const n = idCol.length;
|
|
159
|
+
const rows = [];
|
|
160
|
+
for (let i = 0; i < n; i++) {
|
|
161
|
+
const row = {};
|
|
162
|
+
for (const k of keys) {
|
|
163
|
+
const col = data[k];
|
|
164
|
+
row[k] = Array.isArray(col) ? col[i] : void 0;
|
|
165
|
+
}
|
|
166
|
+
rows.push(row);
|
|
167
|
+
}
|
|
168
|
+
return rows;
|
|
169
|
+
}
|
|
170
|
+
function manualSortKey(row) {
|
|
171
|
+
const v = row.manualSort;
|
|
172
|
+
if (typeof v === "number" && !Number.isNaN(v)) return v;
|
|
173
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
174
|
+
const n = Number(v);
|
|
175
|
+
if (!Number.isNaN(n)) return n;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
function sortRowsByGristOrder(rows) {
|
|
180
|
+
const hasManual = rows.some((r) => manualSortKey(r) !== null);
|
|
181
|
+
if (!hasManual) {
|
|
182
|
+
return [...rows].sort((a, b) => {
|
|
183
|
+
const ida = typeof a.id === "number" ? a.id : 0;
|
|
184
|
+
const idb = typeof b.id === "number" ? b.id : 0;
|
|
185
|
+
return ida - idb;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return [...rows].sort((a, b) => {
|
|
189
|
+
const ma = manualSortKey(a);
|
|
190
|
+
const mb = manualSortKey(b);
|
|
191
|
+
if (ma !== null && mb !== null && ma !== mb) return ma - mb;
|
|
192
|
+
if (ma !== null && mb === null) return -1;
|
|
193
|
+
if (ma === null && mb !== null) return 1;
|
|
194
|
+
const ida = typeof a.id === "number" ? a.id : 0;
|
|
195
|
+
const idb = typeof b.id === "number" ? b.id : 0;
|
|
196
|
+
return ida - idb;
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
function materializeGristRowsFromColumnar(data, options) {
|
|
200
|
+
const order = options?.order ?? "grist";
|
|
201
|
+
let rows;
|
|
202
|
+
if (options?.columns) {
|
|
203
|
+
const columnIds = Object.keys(data);
|
|
204
|
+
rows = decodeColumnarToReplicaRows(data, columnIds, options.columns);
|
|
205
|
+
} else {
|
|
206
|
+
rows = tableDataToRows(data);
|
|
207
|
+
}
|
|
208
|
+
if (order === "none") return rows;
|
|
209
|
+
if (order === "id") {
|
|
210
|
+
return [...rows].sort((a, b) => {
|
|
211
|
+
const ida = typeof a.id === "number" ? a.id : 0;
|
|
212
|
+
const idb = typeof b.id === "number" ? b.id : 0;
|
|
213
|
+
return ida - idb;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return sortRowsByGristOrder(rows);
|
|
217
|
+
}
|
|
218
|
+
async function fetchGristTableRows(docApi, tableId, options) {
|
|
219
|
+
const data = await docApi.fetchTable(tableId);
|
|
220
|
+
return materializeGristRowsFromColumnar(data, options);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/sdk/helpers/grist-widget-options.ts
|
|
224
|
+
function parseGristWidgetOptionsCell(raw) {
|
|
225
|
+
if (raw == null || raw === "") return void 0;
|
|
226
|
+
if (typeof raw === "string") {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(raw);
|
|
229
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
230
|
+
return parsed;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
if (typeof raw === "object" && !Array.isArray(raw)) {
|
|
238
|
+
return raw;
|
|
239
|
+
}
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/sdk/helpers/build-replica-document.ts
|
|
244
|
+
function readCellStr(table, key, rowIndex) {
|
|
245
|
+
const col = table[key];
|
|
246
|
+
if (!Array.isArray(col)) return void 0;
|
|
247
|
+
const v = col[rowIndex];
|
|
248
|
+
if (v === null || v === void 0) return void 0;
|
|
249
|
+
if (typeof v === "string") return v;
|
|
250
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
function readCellNum(table, key, rowIndex) {
|
|
254
|
+
const col = table[key];
|
|
255
|
+
if (!Array.isArray(col)) return void 0;
|
|
256
|
+
const v = col[rowIndex];
|
|
257
|
+
if (typeof v === "number" && !Number.isNaN(v)) return v;
|
|
258
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
259
|
+
const n = Number(v);
|
|
260
|
+
if (!Number.isNaN(n)) return n;
|
|
261
|
+
}
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
async function loadColumnMetadataByTable(docApi) {
|
|
265
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
266
|
+
try {
|
|
267
|
+
const tables = await docApi.fetchTable("_grist_Tables");
|
|
268
|
+
const columns = await docApi.fetchTable("_grist_Tables_column");
|
|
269
|
+
const tableIds = tables.tableId;
|
|
270
|
+
const tableInternalIds = tables.id;
|
|
271
|
+
if (!Array.isArray(tableIds) || !Array.isArray(tableInternalIds)) return byTable;
|
|
272
|
+
const internalIdToTableId = /* @__PURE__ */ new Map();
|
|
273
|
+
for (let i = 0; i < tableInternalIds.length; i++) {
|
|
274
|
+
const tid = tableIds[i];
|
|
275
|
+
const iid = coerceGristDocApiRefRowId(tableInternalIds[i]);
|
|
276
|
+
if (typeof tid === "string" && iid !== void 0) internalIdToTableId.set(iid, tid);
|
|
277
|
+
}
|
|
278
|
+
const parentIds = columns.parentId;
|
|
279
|
+
const colIds = columns.colId;
|
|
280
|
+
const columnIds = columns.id;
|
|
281
|
+
if (!Array.isArray(parentIds) || !Array.isArray(colIds) || !Array.isArray(columnIds) || parentIds.length !== colIds.length || colIds.length !== columnIds.length) {
|
|
282
|
+
return byTable;
|
|
283
|
+
}
|
|
284
|
+
const n = parentIds.length;
|
|
285
|
+
for (let i = 0; i < n; i++) {
|
|
286
|
+
const parentRowId = coerceGristDocApiRefRowId(parentIds[i]);
|
|
287
|
+
const colRef = coerceGristDocApiRefRowId(columnIds[i]);
|
|
288
|
+
const colId = colIds[i];
|
|
289
|
+
if (parentRowId === void 0 || colRef === void 0 || typeof colId !== "string") continue;
|
|
290
|
+
const tableId = internalIdToTableId.get(parentRowId);
|
|
291
|
+
if (!tableId) continue;
|
|
292
|
+
let colMap = byTable.get(tableId);
|
|
293
|
+
if (!colMap) {
|
|
294
|
+
colMap = /* @__PURE__ */ new Map();
|
|
295
|
+
byTable.set(tableId, colMap);
|
|
296
|
+
}
|
|
297
|
+
const parentPos = readCellNum(columns, "parentPos", i);
|
|
298
|
+
const isFormulaCol = columns.isFormula;
|
|
299
|
+
const isFormula = Array.isArray(isFormulaCol) ? Boolean(isFormulaCol[i]) : false;
|
|
300
|
+
const woCol = columns.widgetOptions;
|
|
301
|
+
const widgetOptions = Array.isArray(woCol) && i < woCol.length ? parseGristWidgetOptionsCell(woCol[i]) : void 0;
|
|
302
|
+
const displayColRef = coerceGristDocApiRefRowId(
|
|
303
|
+
Array.isArray(columns.displayCol) ? columns.displayCol[i] : void 0
|
|
304
|
+
);
|
|
305
|
+
const visibleColRef = coerceGristDocApiRefRowId(
|
|
306
|
+
Array.isArray(columns.visibleCol) ? columns.visibleCol[i] : void 0
|
|
307
|
+
);
|
|
308
|
+
colMap.set(colId, {
|
|
309
|
+
colRef,
|
|
310
|
+
type: readCellStr(columns, "type", i),
|
|
311
|
+
label: readCellStr(columns, "label", i),
|
|
312
|
+
parentPos: parentPos !== void 0 ? parentPos : void 0,
|
|
313
|
+
isFormula,
|
|
314
|
+
formula: readCellStr(columns, "formula", i),
|
|
315
|
+
description: readCellStr(columns, "description", i) ?? readCellStr(columns, "descr", i),
|
|
316
|
+
widgetOptions,
|
|
317
|
+
displayColRef,
|
|
318
|
+
visibleColRef
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
return byTable;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/sdk/helpers/grist-columns-filters.ts
|
|
327
|
+
var NOISE_COLUMN_FORMULAS = /* @__PURE__ */ new Set(["now()"]);
|
|
328
|
+
function normalizeFormulaForComparison(formula) {
|
|
329
|
+
return formula.trim().toLowerCase().replace(/\s+/g, "");
|
|
330
|
+
}
|
|
331
|
+
function isGristNoiseColumn(colId, col, options) {
|
|
332
|
+
const excludeId = options?.excludeId ?? true;
|
|
333
|
+
const excludeGristHelpers = options?.excludeGristHelpers ?? true;
|
|
334
|
+
const excludeLoggingColumns = options?.excludeLoggingColumns ?? true;
|
|
335
|
+
const excludeManualSort = options?.excludeManualSort ?? false;
|
|
336
|
+
if (excludeId && colId === "id") return true;
|
|
337
|
+
if (excludeManualSort && colId === "manualSort") return true;
|
|
338
|
+
if (excludeGristHelpers && colId.startsWith("gristHelper_")) return true;
|
|
339
|
+
if (excludeLoggingColumns) {
|
|
340
|
+
const formula = typeof col?.formula === "string" ? normalizeFormulaForComparison(col.formula) : "";
|
|
341
|
+
if (formula && NOISE_COLUMN_FORMULAS.has(formula)) return true;
|
|
342
|
+
if (formula.startsWith("user.")) return true;
|
|
343
|
+
}
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/sdk/helpers/grist-attachment-download.ts
|
|
348
|
+
async function getAttachmentDownloadUrl(docApi, attachmentId, options) {
|
|
349
|
+
const getAccessToken = docApi.getAccessToken;
|
|
350
|
+
if (!getAccessToken) {
|
|
351
|
+
throw new Error("getAccessToken is not available on docApi");
|
|
352
|
+
}
|
|
353
|
+
const { token: token2, baseUrl } = await getAccessToken({ readOnly: options?.readOnly ?? false });
|
|
354
|
+
return `${baseUrl}/attachments/${attachmentId}/download?auth=${token2}`;
|
|
355
|
+
}
|
|
356
|
+
async function fetchBytesFromSignedAttachmentUrl(url) {
|
|
357
|
+
const response = await fetch(url);
|
|
358
|
+
const contentType = response.headers.get("content-type")?.trim() || "application/octet-stream";
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
throw new Error(`Attachment download failed (${response.status})`);
|
|
361
|
+
}
|
|
362
|
+
const buffer = await response.arrayBuffer();
|
|
363
|
+
return { buffer, contentType };
|
|
364
|
+
}
|
|
365
|
+
async function fetchAttachmentBlob(docApi, attachmentId, options) {
|
|
366
|
+
const url = await getAttachmentDownloadUrl(docApi, attachmentId, options);
|
|
367
|
+
const { buffer, contentType } = await fetchBytesFromSignedAttachmentUrl(url);
|
|
368
|
+
return { blob: new Blob([buffer], { type: contentType }), contentType };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/sdk/helpers/grist-fetch-with-auth.ts
|
|
372
|
+
var TOKEN_TTL_MS = 45e3;
|
|
373
|
+
var cache = /* @__PURE__ */ new Map();
|
|
374
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
375
|
+
function cacheKey(readOnly) {
|
|
376
|
+
return readOnly ? "ro" : "rw";
|
|
377
|
+
}
|
|
378
|
+
async function getGristAccessToken(docApi, options) {
|
|
379
|
+
if (!docApi.getAccessToken) {
|
|
380
|
+
throw new Error("getAccessToken is not available on docApi (need higher access level)");
|
|
381
|
+
}
|
|
382
|
+
const readOnly = options?.readOnly ?? false;
|
|
383
|
+
const key = cacheKey(readOnly);
|
|
384
|
+
const now = Date.now();
|
|
385
|
+
const hit = cache.get(key);
|
|
386
|
+
if (hit && now - hit.fetchedAt < TOKEN_TTL_MS) {
|
|
387
|
+
return hit.token;
|
|
388
|
+
}
|
|
389
|
+
const pending = inFlight.get(key);
|
|
390
|
+
if (pending) return pending;
|
|
391
|
+
const next = docApi.getAccessToken({ readOnly }).then((token2) => {
|
|
392
|
+
cache.set(key, { token: token2, fetchedAt: Date.now() });
|
|
393
|
+
inFlight.delete(key);
|
|
394
|
+
return token2;
|
|
395
|
+
});
|
|
396
|
+
inFlight.set(key, next);
|
|
397
|
+
return next;
|
|
398
|
+
}
|
|
399
|
+
function clearGristAccessTokenCache() {
|
|
400
|
+
cache.clear();
|
|
401
|
+
inFlight.clear();
|
|
402
|
+
}
|
|
403
|
+
async function fetchWithGristAuth(docApi, path, init) {
|
|
404
|
+
const readOnly = init?.readOnly ?? false;
|
|
405
|
+
let token2;
|
|
406
|
+
try {
|
|
407
|
+
token2 = await getGristAccessToken(docApi, { readOnly });
|
|
408
|
+
} catch (err) {
|
|
409
|
+
clearGristAccessTokenCache();
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
const isAbsolute = /^https?:\/\//i.test(path);
|
|
413
|
+
const cleanPath = isAbsolute ? path : path.startsWith("/") ? path : `/${path}`;
|
|
414
|
+
const url = isAbsolute ? new URL(path) : new URL(`${token2.baseUrl.replace(/\/$/, "")}${cleanPath}`);
|
|
415
|
+
const gristOrigin = new URL(token2.baseUrl).origin;
|
|
416
|
+
if (url.origin !== gristOrigin) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`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.`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (!url.searchParams.has("auth")) url.searchParams.set("auth", token2.token);
|
|
422
|
+
const headers = new Headers(init?.headers ?? {});
|
|
423
|
+
headers.delete("Authorization");
|
|
424
|
+
const response = await fetch(url.toString(), { ...init, headers });
|
|
425
|
+
if (response.status === 401) {
|
|
426
|
+
clearGristAccessTokenCache();
|
|
427
|
+
const retryToken = await getGristAccessToken(docApi, { readOnly });
|
|
428
|
+
url.searchParams.set("auth", retryToken.token);
|
|
429
|
+
return fetch(url.toString(), { ...init, headers });
|
|
430
|
+
}
|
|
431
|
+
return response;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/sdk/helpers/grist-attachment-upload.ts
|
|
435
|
+
function parseGristAttachmentUploadResponseIds(raw, options) {
|
|
436
|
+
const trimmed = raw.trim();
|
|
437
|
+
if (!trimmed) {
|
|
438
|
+
throw new Error("Attachment upload returned an empty response.");
|
|
439
|
+
}
|
|
440
|
+
let parsed;
|
|
441
|
+
try {
|
|
442
|
+
parsed = JSON.parse(trimmed);
|
|
443
|
+
} catch {
|
|
444
|
+
if (options?.strict) {
|
|
445
|
+
throw new Error("Attachment upload returned non-JSON response.");
|
|
446
|
+
}
|
|
447
|
+
parsed = trimmed;
|
|
448
|
+
}
|
|
449
|
+
return extractAttachmentIdsFromUploadPayload(parsed, options);
|
|
450
|
+
}
|
|
451
|
+
function extractAttachmentIdsFromUploadPayload(parsed, options) {
|
|
452
|
+
if (options?.strict) {
|
|
453
|
+
return extractAttachmentIdsFromUploadPayloadStrict(parsed);
|
|
454
|
+
}
|
|
455
|
+
const first = extractFirstAttachmentIdFromUploadPayload(parsed);
|
|
456
|
+
if (first !== void 0) {
|
|
457
|
+
if (Array.isArray(parsed)) {
|
|
458
|
+
const ids = [];
|
|
459
|
+
const seen = /* @__PURE__ */ new Set();
|
|
460
|
+
for (const item of parsed) {
|
|
461
|
+
const id = coerceAttachmentId(item);
|
|
462
|
+
if (id && !seen.has(id)) {
|
|
463
|
+
seen.add(id);
|
|
464
|
+
ids.push(id);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return ids.length > 0 ? ids : [first];
|
|
468
|
+
}
|
|
469
|
+
return [first];
|
|
470
|
+
}
|
|
471
|
+
throw new Error("Attachment upload returned an unexpected response shape.");
|
|
472
|
+
}
|
|
473
|
+
function extractAttachmentIdsFromUploadPayloadStrict(parsed) {
|
|
474
|
+
if (typeof parsed === "number" && Number.isFinite(parsed)) {
|
|
475
|
+
return [String(parsed)];
|
|
476
|
+
}
|
|
477
|
+
if (Array.isArray(parsed)) {
|
|
478
|
+
const ids = [];
|
|
479
|
+
const seen = /* @__PURE__ */ new Set();
|
|
480
|
+
for (const item of parsed) {
|
|
481
|
+
if (typeof item !== "number" || !Number.isFinite(item)) {
|
|
482
|
+
throw new Error("Attachment upload returned an unexpected response shape.");
|
|
483
|
+
}
|
|
484
|
+
const id = String(item);
|
|
485
|
+
if (!seen.has(id)) {
|
|
486
|
+
seen.add(id);
|
|
487
|
+
ids.push(id);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (ids.length === 0) {
|
|
491
|
+
throw new Error("Attachment upload returned an empty id list.");
|
|
492
|
+
}
|
|
493
|
+
return ids;
|
|
494
|
+
}
|
|
495
|
+
throw new Error("Attachment upload returned an unexpected response shape.");
|
|
496
|
+
}
|
|
497
|
+
function extractFirstAttachmentIdFromUploadPayload(parsed) {
|
|
498
|
+
if (typeof parsed === "number" && Number.isFinite(parsed)) {
|
|
499
|
+
return String(parsed);
|
|
500
|
+
}
|
|
501
|
+
if (typeof parsed === "string" && parsed.trim()) {
|
|
502
|
+
return parsed.trim();
|
|
503
|
+
}
|
|
504
|
+
if (Array.isArray(parsed)) {
|
|
505
|
+
for (const item of parsed) {
|
|
506
|
+
const id = coerceAttachmentId(item);
|
|
507
|
+
if (id) return id;
|
|
508
|
+
}
|
|
509
|
+
return void 0;
|
|
510
|
+
}
|
|
511
|
+
if (parsed && typeof parsed === "object") {
|
|
512
|
+
const record = parsed;
|
|
513
|
+
for (const key of ["id", "attachmentId", "fileId"]) {
|
|
514
|
+
const id = coerceAttachmentId(record[key]);
|
|
515
|
+
if (id) return id;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return void 0;
|
|
519
|
+
}
|
|
520
|
+
function coerceAttachmentId(value) {
|
|
521
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
522
|
+
return String(value);
|
|
523
|
+
}
|
|
524
|
+
if (typeof value === "string" && value.trim()) {
|
|
525
|
+
return value.trim();
|
|
526
|
+
}
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
var UPLOAD_HEADERS = {
|
|
530
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
531
|
+
Accept: "application/json"
|
|
532
|
+
};
|
|
533
|
+
async function uploadGristAttachment(docApi, file, options) {
|
|
534
|
+
const form = new FormData();
|
|
535
|
+
const fileName = options?.fileName ?? (file instanceof File && file.name ? file.name : "upload");
|
|
536
|
+
form.append("upload", file, fileName);
|
|
537
|
+
let response;
|
|
538
|
+
try {
|
|
539
|
+
response = await fetchWithGristAuth(docApi, "/attachments", {
|
|
540
|
+
method: "POST",
|
|
541
|
+
body: form,
|
|
542
|
+
readOnly: false,
|
|
543
|
+
headers: UPLOAD_HEADERS
|
|
544
|
+
});
|
|
545
|
+
} catch (err) {
|
|
546
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
547
|
+
if (/failed to fetch/i.test(msg)) {
|
|
548
|
+
const widgetOrigin = typeof globalThis.location !== "undefined" ? globalThis.location.origin : "this widget origin";
|
|
549
|
+
throw new Error(
|
|
550
|
+
`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).`,
|
|
551
|
+
{ cause: err }
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
556
|
+
const raw = await response.text();
|
|
557
|
+
if (!response.ok) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
raw.trim() ? `Attachment upload failed (${response.status}): ${raw.slice(0, 200)}` : `Attachment upload failed (${response.status})`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
const ids = parseGristAttachmentUploadResponseIds(raw);
|
|
563
|
+
return { ids, firstId: ids[0] };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/sdk/runtime/api.ts
|
|
567
|
+
var ACCESS_ERROR = [/access not granted/i, /need higher access level/i, /requires.*full.*access/i, /RPC_ONCALL_ERROR Access/i];
|
|
568
|
+
function isAccessInsufficientError(message2) {
|
|
569
|
+
return ACCESS_ERROR.some((re) => re.test(message2));
|
|
570
|
+
}
|
|
571
|
+
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
572
|
+
function createGristApi(runtime) {
|
|
573
|
+
const docApi = () => {
|
|
574
|
+
if (runtime.getState().connection !== "ready" || typeof grist === "undefined" || !grist.docApi) {
|
|
575
|
+
throw new Error('Grist is not ready: wait for useGrist().status === "ready".');
|
|
576
|
+
}
|
|
577
|
+
return grist.docApi;
|
|
578
|
+
};
|
|
579
|
+
const read = async (fn) => {
|
|
580
|
+
try {
|
|
581
|
+
const out = await fn();
|
|
582
|
+
if (runtime.getState().readError) runtime.update({ readError: null });
|
|
583
|
+
return out;
|
|
584
|
+
} catch (err) {
|
|
585
|
+
if (isAccessInsufficientError(message(err))) runtime.update({ readError: message(err) });
|
|
586
|
+
throw err;
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
const write = async (fn) => {
|
|
590
|
+
runtime.update({ actionStatus: "running", actionError: null });
|
|
591
|
+
try {
|
|
592
|
+
const out = await fn();
|
|
593
|
+
runtime.update({ actionStatus: "idle" });
|
|
594
|
+
return out;
|
|
595
|
+
} catch (err) {
|
|
596
|
+
runtime.update({ actionStatus: "error", actionError: message(err) });
|
|
597
|
+
throw err;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
const tableOps = (resolve) => ({
|
|
601
|
+
getTableId: () => resolve().getTableId(),
|
|
602
|
+
create: ((records, opts) => write(() => resolve().create(records, opts))),
|
|
603
|
+
update: (records, opts) => write(() => resolve().update(records, opts)),
|
|
604
|
+
upsert: (records, opts) => write(() => resolve().upsert(records, opts)),
|
|
605
|
+
destroy: (ids) => write(() => resolve().destroy(ids))
|
|
606
|
+
});
|
|
607
|
+
const tables = /* @__PURE__ */ new Map();
|
|
608
|
+
return {
|
|
609
|
+
reload: () => runtime.reload(),
|
|
610
|
+
fetchSelectedTable: (options) => read(async () => {
|
|
611
|
+
const impl = docApi().fetchSelectedTable ?? grist.fetchSelectedTable;
|
|
612
|
+
if (!impl) throw new Error("fetchSelectedTable is not available in this Grist version");
|
|
613
|
+
return await impl({ keepEncoded: false, ...options });
|
|
614
|
+
}),
|
|
615
|
+
fetchTable: (tableId) => read(async () => await docApi().fetchTable(tableId)),
|
|
616
|
+
fetchTableRows: (tableId, options) => read(() => fetchGristTableRows(docApi(), tableId, options ?? {})),
|
|
617
|
+
listTables: (options) => read(async () => {
|
|
618
|
+
const all = [...await docApi().listTables()];
|
|
619
|
+
return options?.includeSystem ? all : all.filter((t) => !t.startsWith("_grist") && !t.startsWith("GristHidden_"));
|
|
620
|
+
}),
|
|
621
|
+
listColumns: (tableId, options) => read(async () => {
|
|
622
|
+
const meta = (await loadColumnMetadataByTable(docApi())).get(tableId);
|
|
623
|
+
if (!meta) return [];
|
|
624
|
+
const noise = {
|
|
625
|
+
excludeId: options?.excludeId ?? true,
|
|
626
|
+
excludeGristHelpers: options?.excludeGristHelpers ?? true,
|
|
627
|
+
excludeLoggingColumns: options?.excludeLoggingColumns ?? true,
|
|
628
|
+
excludeManualSort: options?.excludeManualSort ?? true
|
|
629
|
+
};
|
|
630
|
+
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 }));
|
|
631
|
+
}),
|
|
632
|
+
table: tableOps(() => {
|
|
633
|
+
if (!grist.selectedTable) throw new Error("grist.selectedTable is not available in this Grist version");
|
|
634
|
+
return grist.selectedTable;
|
|
635
|
+
}),
|
|
636
|
+
getTable: (tableId) => {
|
|
637
|
+
let ops = tables.get(tableId);
|
|
638
|
+
if (!ops) {
|
|
639
|
+
tables.set(
|
|
640
|
+
tableId,
|
|
641
|
+
ops = tableOps(() => {
|
|
642
|
+
if (typeof grist.getTable !== "function") throw new Error("grist.getTable is not available in this Grist version");
|
|
643
|
+
return grist.getTable(tableId);
|
|
644
|
+
})
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
return ops;
|
|
648
|
+
},
|
|
649
|
+
applyActions: (actions, options) => write(() => docApi().applyUserActions(actions, options)),
|
|
650
|
+
setWidgetOptions: async (options) => {
|
|
651
|
+
await grist.setOptions(options);
|
|
652
|
+
},
|
|
653
|
+
patchWidgetOptions: async (patch) => {
|
|
654
|
+
const current = await grist.getOptions() ?? {};
|
|
655
|
+
await grist.setOptions({ ...current, ...patch });
|
|
656
|
+
},
|
|
657
|
+
setCursorPosition: async (pos) => {
|
|
658
|
+
if (!grist.setCursorPos) throw new Error("setCursorPos is not available in this Grist version");
|
|
659
|
+
await grist.setCursorPos(pos);
|
|
660
|
+
},
|
|
661
|
+
setLinkedRowSelection: async (rowIds) => {
|
|
662
|
+
if (!grist.setSelectedRows) throw new Error("setSelectedRows is not available in this Grist version");
|
|
663
|
+
await grist.setSelectedRows(rowIds);
|
|
664
|
+
},
|
|
665
|
+
getAttachmentUrl: (id, options) => read(() => getAttachmentDownloadUrl(docApi(), id, options)),
|
|
666
|
+
fetchAttachmentBlob: (id, options) => read(() => fetchAttachmentBlob(docApi(), id, options)),
|
|
667
|
+
uploadAttachment: (file, options) => write(() => uploadGristAttachment(docApi(), file, options)),
|
|
668
|
+
fetchWithAuth: (path, init) => read(() => fetchWithGristAuth(docApi(), path, init))
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/sdk/helpers/grist-mappings.ts
|
|
673
|
+
function asColumnSpec(entry) {
|
|
674
|
+
return typeof entry === "string" ? { name: entry } : entry;
|
|
675
|
+
}
|
|
676
|
+
function validateColumnMappings(spec, mappings) {
|
|
677
|
+
const missing = [];
|
|
678
|
+
const emptyMultiples = [];
|
|
679
|
+
if (!spec) return { ok: true, pending: false, missing, emptyMultiples };
|
|
680
|
+
const m = mappings ?? {};
|
|
681
|
+
for (const raw of spec) {
|
|
682
|
+
const col = asColumnSpec(raw);
|
|
683
|
+
if (col.optional) continue;
|
|
684
|
+
const value = m[col.name];
|
|
685
|
+
if (col.allowMultiple) {
|
|
686
|
+
if (value === void 0) {
|
|
687
|
+
missing.push(col.name);
|
|
688
|
+
} else if (!Array.isArray(value) || value.length === 0) {
|
|
689
|
+
emptyMultiples.push(col.name);
|
|
690
|
+
}
|
|
691
|
+
} else {
|
|
692
|
+
if (value === void 0 || value === null || value === "") missing.push(col.name);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
ok: missing.length === 0 && emptyMultiples.length === 0,
|
|
697
|
+
pending: false,
|
|
698
|
+
missing,
|
|
699
|
+
emptyMultiples
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/sdk/runtime/derive.ts
|
|
704
|
+
var ACCESS_RANK = { none: 0, "read table": 1, full: 2 };
|
|
705
|
+
function parseAccess(raw) {
|
|
706
|
+
return raw === "none" || raw === "read table" || raw === "full" ? raw : null;
|
|
707
|
+
}
|
|
708
|
+
function accessLabel(level) {
|
|
709
|
+
return level === "full" ? "Full document access" : level === "read table" ? "Read selected table" : "No document access";
|
|
710
|
+
}
|
|
711
|
+
function accessDenied(state) {
|
|
712
|
+
return state.grantedAccess !== null && ACCESS_RANK[state.grantedAccess] < ACCESS_RANK[state.declaredAccess];
|
|
713
|
+
}
|
|
714
|
+
function deriveStatus(state) {
|
|
715
|
+
switch (state.connection) {
|
|
716
|
+
case "connecting":
|
|
717
|
+
return "booting";
|
|
718
|
+
case "not-embedded":
|
|
719
|
+
case "unavailable":
|
|
720
|
+
return "unavailable";
|
|
721
|
+
case "error":
|
|
722
|
+
return "error";
|
|
723
|
+
case "ready":
|
|
724
|
+
return accessDenied(state) ? "error" : "ready";
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
var MAPPINGS_OK = Object.freeze({ ok: true, pending: false, missing: [], emptyMultiples: [] });
|
|
728
|
+
var MAPPINGS_PENDING = Object.freeze({ ok: true, pending: true, missing: [], emptyMultiples: [] });
|
|
729
|
+
function deriveColumnMappingStatus(columns, mappings) {
|
|
730
|
+
if (!columns?.length) return MAPPINGS_OK;
|
|
731
|
+
if (mappings === null) return MAPPINGS_PENDING;
|
|
732
|
+
return validateColumnMappings(columns, mappings);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/sdk/runtime/cells.ts
|
|
736
|
+
var ok = (value) => ({ ok: true, value });
|
|
737
|
+
var bad = (message2) => ({ ok: false, message: message2 });
|
|
738
|
+
function formulaError(value) {
|
|
739
|
+
if (Array.isArray(value) && value[0] === "E") return String(value[1] ?? "error");
|
|
740
|
+
if (value && typeof value === "object" && value.constructor?.name === "RaisedException") {
|
|
741
|
+
return String(value.name ?? "error");
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
function rowIdOf(value) {
|
|
746
|
+
if (typeof value === "number" && Number.isInteger(value)) return value === 0 ? null : value;
|
|
747
|
+
if (value && typeof value === "object" && typeof value.rowId === "number") {
|
|
748
|
+
const id = value.rowId;
|
|
749
|
+
return id === 0 ? null : id;
|
|
750
|
+
}
|
|
751
|
+
if (Array.isArray(value) && value[0] === "R" && typeof value[2] === "number") return value[2] === 0 ? null : value[2];
|
|
752
|
+
return void 0;
|
|
753
|
+
}
|
|
754
|
+
function idsOf(value) {
|
|
755
|
+
if (value === null || value === void 0) return [];
|
|
756
|
+
const list = Array.isArray(value) ? value[0] === "L" ? value.slice(1) : value : value && typeof value === "object" && Array.isArray(value.rowIds) ? value.rowIds : void 0;
|
|
757
|
+
return list?.every((v) => typeof v === "number") ? list : void 0;
|
|
758
|
+
}
|
|
759
|
+
function checkOne(base, value) {
|
|
760
|
+
switch (base) {
|
|
761
|
+
case "Any":
|
|
762
|
+
return ok(value);
|
|
763
|
+
case "Text":
|
|
764
|
+
return value === null || typeof value === "string" ? ok(value ?? "") : bad("not text");
|
|
765
|
+
case "Numeric":
|
|
766
|
+
return value === null || typeof value === "number" && Number.isFinite(value) ? ok(value) : bad("not a number");
|
|
767
|
+
case "Int":
|
|
768
|
+
return value === null || typeof value === "number" && Number.isInteger(value) ? ok(value) : bad("not an integer");
|
|
769
|
+
case "Bool":
|
|
770
|
+
return value === null || typeof value === "boolean" ? ok(value ?? false) : bad("not true or false");
|
|
771
|
+
case "Date":
|
|
772
|
+
case "DateTime":
|
|
773
|
+
if (value === null) return ok(null);
|
|
774
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return ok(value);
|
|
775
|
+
if (typeof value === "number" && Number.isFinite(value)) return ok(new Date(value * 1e3));
|
|
776
|
+
return bad("not a date");
|
|
777
|
+
case "Choice":
|
|
778
|
+
return value === null || typeof value === "string" ? ok(value) : bad("not a choice");
|
|
779
|
+
case "ChoiceList": {
|
|
780
|
+
if (value === null || value === void 0) return ok([]);
|
|
781
|
+
const list = Array.isArray(value) ? value[0] === "L" ? value.slice(1) : value : void 0;
|
|
782
|
+
return list?.every((v) => typeof v === "string") ? ok(list) : bad("not a list of choices");
|
|
783
|
+
}
|
|
784
|
+
case "Ref": {
|
|
785
|
+
if (value === null) return ok(null);
|
|
786
|
+
const id = rowIdOf(value);
|
|
787
|
+
return id === void 0 ? bad("not a reference") : ok(id);
|
|
788
|
+
}
|
|
789
|
+
case "RefList":
|
|
790
|
+
case "Attachments": {
|
|
791
|
+
const ids = idsOf(value);
|
|
792
|
+
return ids === void 0 ? bad(base === "RefList" ? "not a list of references" : "not a list of attachments") : ok(ids);
|
|
793
|
+
}
|
|
794
|
+
default:
|
|
795
|
+
return ok(value);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function checkGristCell(type, value) {
|
|
799
|
+
const error = formulaError(value);
|
|
800
|
+
if (error) return bad(`formula error (${error})`);
|
|
801
|
+
const bases = (type ?? "Any").split(",").map((t) => t.trim().split(":")[0]).filter(Boolean);
|
|
802
|
+
if (bases.length === 0 || bases.includes("Any")) return ok(value === void 0 ? null : value);
|
|
803
|
+
let first = null;
|
|
804
|
+
for (const base of bases) {
|
|
805
|
+
const check = checkOne(base, value);
|
|
806
|
+
if (check.ok) return check;
|
|
807
|
+
first ?? (first = check);
|
|
808
|
+
}
|
|
809
|
+
return first;
|
|
810
|
+
}
|
|
811
|
+
var nameOf = (c) => typeof c === "string" ? c : c.name;
|
|
812
|
+
var typeOf = (c) => typeof c === "string" ? void 0 : c.type;
|
|
813
|
+
var isOptional = (c) => typeof c === "object" && c.optional === true;
|
|
814
|
+
function mapAndCheck(record, columns, mappings, schema) {
|
|
815
|
+
if (!record) return { value: null, issues: [] };
|
|
816
|
+
if (!columns?.length) return { value: record, issues: [] };
|
|
817
|
+
if (mappings === null) return { value: null, issues: [] };
|
|
818
|
+
const rowId = typeof record.id === "number" ? record.id : 0;
|
|
819
|
+
const out = { id: record.id };
|
|
820
|
+
const issues = [];
|
|
821
|
+
for (const col of columns) {
|
|
822
|
+
const name = nameOf(col);
|
|
823
|
+
const real = mappings[name];
|
|
824
|
+
const take = (raw) => {
|
|
825
|
+
const check = checkGristCell(typeOf(col), raw);
|
|
826
|
+
if (check.ok) return check.value;
|
|
827
|
+
issues.push({ rowId, column: name, message: check.message, value: raw });
|
|
828
|
+
return null;
|
|
829
|
+
};
|
|
830
|
+
if (Array.isArray(real) && real.length > 0) out[name] = real.map((r) => take(record[r]));
|
|
831
|
+
else if (typeof real === "string" && real) out[name] = take(record[real]);
|
|
832
|
+
else if (!isOptional(col)) return { value: null, issues: [] };
|
|
833
|
+
}
|
|
834
|
+
if (schema) {
|
|
835
|
+
const result = schema["~standard"].validate(out);
|
|
836
|
+
if (result instanceof Promise) {
|
|
837
|
+
issues.push({ rowId, column: "", message: "the schema validates asynchronously; useGrist() needs a synchronous one", value: out });
|
|
838
|
+
} else if ("issues" in result && result.issues) {
|
|
839
|
+
for (const issue of result.issues) {
|
|
840
|
+
const key = issue.path?.[0];
|
|
841
|
+
const column = key === void 0 ? "" : String(typeof key === "object" ? key.key : key);
|
|
842
|
+
issues.push({ rowId, column, message: issue.message, value: column ? out[column] : out });
|
|
843
|
+
}
|
|
844
|
+
} else if ("value" in result) {
|
|
845
|
+
return { value: result.value, issues };
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return { value: out, issues };
|
|
849
|
+
}
|
|
40
850
|
|
|
41
851
|
// src/sdk/helpers/grist-widget-interaction.ts
|
|
42
852
|
function parseGristWidgetLinkingInfo(raw) {
|
|
@@ -59,152 +869,363 @@ function normalizeWidgetInteraction(interaction) {
|
|
|
59
869
|
};
|
|
60
870
|
}
|
|
61
871
|
|
|
62
|
-
// src/sdk/
|
|
63
|
-
var
|
|
64
|
-
constructor(
|
|
65
|
-
this.
|
|
872
|
+
// src/sdk/runtime/host.ts
|
|
873
|
+
var HostStream = class {
|
|
874
|
+
constructor(bind) {
|
|
875
|
+
this.bind = bind;
|
|
66
876
|
this.listeners = /* @__PURE__ */ new Set();
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
877
|
+
this.bound = false;
|
|
878
|
+
this.has = false;
|
|
69
879
|
}
|
|
70
880
|
subscribe(listener) {
|
|
71
881
|
this.listeners.add(listener);
|
|
72
|
-
if (!this.
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this.last = value;
|
|
80
|
-
for (const l of this.listeners) l(value);
|
|
81
|
-
});
|
|
82
|
-
} catch (err) {
|
|
83
|
-
if (typeof console !== "undefined") {
|
|
84
|
-
console.error("[lib/grist] failed to subscribe to Grist event:", err);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
this.bind();
|
|
89
|
-
}
|
|
90
|
-
if (this.hasValue && this.last !== void 0) {
|
|
91
|
-
listener(this.last);
|
|
882
|
+
if (!this.bound) {
|
|
883
|
+
this.bound = true;
|
|
884
|
+
try {
|
|
885
|
+
this.bind((value) => this.emit(value));
|
|
886
|
+
} catch (err) {
|
|
887
|
+
console.error("[grist-widget-sdk] could not subscribe to a Grist event:", err);
|
|
888
|
+
}
|
|
92
889
|
}
|
|
890
|
+
if (this.has) listener(this.last);
|
|
93
891
|
return () => {
|
|
94
892
|
this.listeners.delete(listener);
|
|
95
893
|
};
|
|
96
894
|
}
|
|
97
|
-
/** Last cached value (or `undefined` if no event has fired yet). */
|
|
98
895
|
peek() {
|
|
99
896
|
return this.last;
|
|
100
897
|
}
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Test helper: fan out a payload without calling `grist.on*`.
|
|
107
|
-
* Used to simulate hosts that reuse one record object and mutate it in place.
|
|
108
|
-
*/
|
|
109
|
-
deliverForTests(value) {
|
|
110
|
-
this.hasValue = true;
|
|
898
|
+
/** Tests: deliver a value as if Grist had sent it. */
|
|
899
|
+
emit(value) {
|
|
900
|
+
this.has = true;
|
|
111
901
|
this.last = value;
|
|
112
|
-
for (const l of this.listeners) l(value);
|
|
902
|
+
for (const l of [...this.listeners]) l(value);
|
|
113
903
|
}
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
* `window.grist` (e.g. via the emulator's `dispose()` + `createGristEmulator()`)
|
|
117
|
-
* and re-bind cleanly on the next subscribe.
|
|
118
|
-
*/
|
|
119
|
-
_resetForTests() {
|
|
904
|
+
/** Tests: forget the binding, so a new `window.grist` binds afresh. */
|
|
905
|
+
reset() {
|
|
120
906
|
this.listeners.clear();
|
|
121
|
-
this.
|
|
907
|
+
this.bound = false;
|
|
908
|
+
this.has = false;
|
|
122
909
|
this.last = void 0;
|
|
123
|
-
this.bind = null;
|
|
124
910
|
}
|
|
125
911
|
};
|
|
126
|
-
var
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
130
|
-
grist.onRecord(
|
|
131
|
-
(record, mappings) => {
|
|
132
|
-
deliver(record, mappings ?? null);
|
|
133
|
-
},
|
|
134
|
-
{ keepEncoded: false }
|
|
135
|
-
);
|
|
912
|
+
var asMappings = (m) => m && typeof m === "object" ? m : null;
|
|
913
|
+
var recordStream = new HostStream((emit) => {
|
|
914
|
+
grist.onRecord((record, mappings) => emit({ record, mappings: asMappings(mappings) }), { keepEncoded: false });
|
|
136
915
|
const fetchSelectedRecord = grist.fetchSelectedRecord ?? grist.docApi?.fetchSelectedRecord;
|
|
137
|
-
if (typeof grist.on !== "function" || typeof fetchSelectedRecord !== "function")
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
916
|
+
if (typeof grist.on !== "function" || typeof fetchSelectedRecord !== "function") return;
|
|
140
917
|
let lastRowId;
|
|
141
918
|
grist.on("message", async (msg) => {
|
|
142
|
-
if (!msg || typeof msg !== "object") return;
|
|
143
919
|
const host = msg;
|
|
144
|
-
if (!host
|
|
145
|
-
if (host.rowId === lastRowId) return;
|
|
920
|
+
if (!host?.tableId || typeof host.rowId !== "number" || host.rowId === lastRowId) return;
|
|
146
921
|
lastRowId = host.rowId;
|
|
147
922
|
try {
|
|
148
923
|
const record = await fetchSelectedRecord(host.rowId, { keepEncoded: false });
|
|
149
|
-
|
|
924
|
+
emit({ record, mappings: recordStream.peek()?.mappings ?? null });
|
|
150
925
|
} catch (err) {
|
|
151
|
-
|
|
152
|
-
console.error("[grist-widget-sdk] fetchSelectedRecord on cursor message failed:", err);
|
|
153
|
-
}
|
|
926
|
+
console.error("[grist-widget-sdk] fetchSelectedRecord on a cursor move failed:", err);
|
|
154
927
|
}
|
|
155
928
|
});
|
|
156
929
|
});
|
|
157
|
-
var
|
|
158
|
-
grist.onRecords(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{ keepEncoded: false, format: "rows" }
|
|
163
|
-
);
|
|
930
|
+
var recordsStream = new HostStream((emit) => {
|
|
931
|
+
grist.onRecords((records, mappings) => emit({ records, mappings: asMappings(mappings) }), {
|
|
932
|
+
keepEncoded: false,
|
|
933
|
+
format: "rows"
|
|
934
|
+
});
|
|
164
935
|
});
|
|
165
|
-
var
|
|
166
|
-
grist.onOptions(
|
|
167
|
-
|
|
168
|
-
interaction ?? null
|
|
169
|
-
);
|
|
170
|
-
emit({
|
|
936
|
+
var optionsStream = new HostStream((emit) => {
|
|
937
|
+
grist.onOptions(
|
|
938
|
+
(options, interaction) => emit({
|
|
171
939
|
options: options ? { ...options } : null,
|
|
172
|
-
interaction:
|
|
173
|
-
})
|
|
174
|
-
|
|
940
|
+
interaction: normalizeWidgetInteraction(interaction ?? null)
|
|
941
|
+
})
|
|
942
|
+
);
|
|
175
943
|
});
|
|
176
|
-
var
|
|
944
|
+
var newRecordStream = new HostStream((emit) => {
|
|
177
945
|
if (typeof grist.onNewRecord !== "function") return;
|
|
178
|
-
grist.onNewRecord((mappings) => {
|
|
179
|
-
emit({ mappings: mappings ?? null });
|
|
180
|
-
});
|
|
946
|
+
grist.onNewRecord((mappings) => emit({ mappings: asMappings(mappings) }));
|
|
181
947
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// src/sdk/internal/embedded-handshake-manager.ts
|
|
190
|
-
var embedded = null;
|
|
191
|
-
function resetEmbeddedHandshakeManagerForTests() {
|
|
192
|
-
if (embedded) {
|
|
193
|
-
embedded.manager.stop();
|
|
194
|
-
embedded = null;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
948
|
+
var messageStream = new HostStream((emit) => {
|
|
949
|
+
if (typeof grist.on !== "function") return;
|
|
950
|
+
grist.on("message", (msg) => emit(msg));
|
|
951
|
+
});
|
|
952
|
+
var ALL_STREAMS = [recordStream, recordsStream, optionsStream, newRecordStream, messageStream];
|
|
197
953
|
|
|
198
|
-
// src/sdk/
|
|
199
|
-
var
|
|
954
|
+
// src/sdk/runtime/ready.ts
|
|
955
|
+
var ACCESS_RANK2 = {
|
|
956
|
+
none: 0,
|
|
957
|
+
"read table": 1,
|
|
958
|
+
full: 2
|
|
959
|
+
};
|
|
960
|
+
var inFlight2 = null;
|
|
200
961
|
var lastAccess = null;
|
|
201
962
|
var lastColumnNames = null;
|
|
202
|
-
function
|
|
203
|
-
|
|
963
|
+
function columnNamesOf(columns) {
|
|
964
|
+
return new Set((columns ?? []).map((c) => typeof c === "string" ? c : c.name));
|
|
965
|
+
}
|
|
966
|
+
function shouldReinit(next, nextColumns) {
|
|
967
|
+
if (!lastAccess) return true;
|
|
968
|
+
if (ACCESS_RANK2[next] > ACCESS_RANK2[lastAccess]) return true;
|
|
969
|
+
for (const name of nextColumns) {
|
|
970
|
+
if (!lastColumnNames?.has(name)) return true;
|
|
971
|
+
}
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
function ensureGristReady(payload) {
|
|
975
|
+
const access = payload.requiredAccess ?? "read table";
|
|
976
|
+
const columns = columnNamesOf(payload.columns);
|
|
977
|
+
if (inFlight2 && !shouldReinit(access, columns)) return inFlight2;
|
|
978
|
+
lastAccess = access;
|
|
979
|
+
lastColumnNames = columns;
|
|
980
|
+
const next = Promise.resolve().then(() => {
|
|
981
|
+
const result = grist.ready(payload);
|
|
982
|
+
return result instanceof Promise ? result : Promise.resolve();
|
|
983
|
+
});
|
|
984
|
+
inFlight2 = next.catch((err) => {
|
|
985
|
+
inFlight2 = null;
|
|
986
|
+
lastAccess = null;
|
|
987
|
+
lastColumnNames = null;
|
|
988
|
+
throw err;
|
|
989
|
+
});
|
|
990
|
+
return inFlight2;
|
|
991
|
+
}
|
|
992
|
+
function resetGristReady() {
|
|
993
|
+
inFlight2 = null;
|
|
204
994
|
lastAccess = null;
|
|
205
995
|
lastColumnNames = null;
|
|
206
996
|
}
|
|
207
|
-
|
|
997
|
+
|
|
998
|
+
// src/sdk/helpers/grist-theme-payload.ts
|
|
999
|
+
function parseGristThemePayload(payload) {
|
|
1000
|
+
if (!payload || typeof payload !== "object") return null;
|
|
1001
|
+
const raw = payload;
|
|
1002
|
+
const top = raw.theme;
|
|
1003
|
+
if (top === "light" || top === "dark") return top;
|
|
1004
|
+
if (top && typeof top === "object") {
|
|
1005
|
+
const appearance = top.appearance;
|
|
1006
|
+
if (appearance === "light" || appearance === "dark") return appearance;
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/sdk/runtime/runtime.ts
|
|
1012
|
+
var READY_TIMEOUT_MS = 3e4;
|
|
1013
|
+
function initialRuntimeState(options = {}) {
|
|
1014
|
+
return {
|
|
1015
|
+
connection: "connecting",
|
|
1016
|
+
connectionError: null,
|
|
1017
|
+
declaredAccess: options.requiredAccess ?? "read table",
|
|
1018
|
+
grantedAccess: null,
|
|
1019
|
+
record: null,
|
|
1020
|
+
records: null,
|
|
1021
|
+
isNewRecord: false,
|
|
1022
|
+
mappings: null,
|
|
1023
|
+
widgetOptions: null,
|
|
1024
|
+
interaction: null,
|
|
1025
|
+
currentTableId: null,
|
|
1026
|
+
theme: null,
|
|
1027
|
+
actionStatus: "idle",
|
|
1028
|
+
actionError: null,
|
|
1029
|
+
readError: null,
|
|
1030
|
+
mapBackSkipped: []
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function isEmbedded() {
|
|
1034
|
+
try {
|
|
1035
|
+
return typeof window !== "undefined" && window.self !== window.top;
|
|
1036
|
+
} catch {
|
|
1037
|
+
return true;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
var hasGrist = () => typeof grist !== "undefined" && grist !== null;
|
|
1041
|
+
function createGristRuntime(options = {}) {
|
|
1042
|
+
let state = initialRuntimeState(options);
|
|
1043
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1044
|
+
let starts = 0;
|
|
1045
|
+
let stopStreams = null;
|
|
1046
|
+
let attempt = 0;
|
|
1047
|
+
const update = (patch) => {
|
|
1048
|
+
let changed = false;
|
|
1049
|
+
for (const key of Object.keys(patch)) {
|
|
1050
|
+
if (!Object.is(state[key], patch[key])) changed = true;
|
|
1051
|
+
}
|
|
1052
|
+
if (!changed) return;
|
|
1053
|
+
state = { ...state, ...patch };
|
|
1054
|
+
for (const l of [...listeners]) l();
|
|
1055
|
+
};
|
|
1056
|
+
const payload = {
|
|
1057
|
+
requiredAccess: options.requiredAccess ?? "read table",
|
|
1058
|
+
columns: options.columns ?? [],
|
|
1059
|
+
...options.allowSelectBy !== void 0 ? { allowSelectBy: options.allowSelectBy } : {},
|
|
1060
|
+
...options.hasCustomOptions !== void 0 ? { hasCustomOptions: options.hasCustomOptions } : {},
|
|
1061
|
+
...options.onEditOptions ? { onEditOptions: options.onEditOptions } : {}
|
|
1062
|
+
};
|
|
1063
|
+
const refreshTable = async () => {
|
|
1064
|
+
try {
|
|
1065
|
+
const tableId = await grist.selectedTable?.getTableId();
|
|
1066
|
+
if (tableId) update({ currentTableId: tableId });
|
|
1067
|
+
} catch {
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
const refreshMappings = async () => {
|
|
1071
|
+
if (typeof grist.sectionApi?.mappings !== "function") return;
|
|
1072
|
+
try {
|
|
1073
|
+
const mappings = await grist.sectionApi.mappings();
|
|
1074
|
+
if (state.mappings === null) update({ mappings: mappings ?? {} });
|
|
1075
|
+
} catch {
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
const connect = async () => {
|
|
1079
|
+
const mine = ++attempt;
|
|
1080
|
+
update({ connection: "connecting", connectionError: null });
|
|
1081
|
+
let timer;
|
|
1082
|
+
try {
|
|
1083
|
+
await Promise.race([
|
|
1084
|
+
ensureGristReady(payload),
|
|
1085
|
+
new Promise((_, reject) => {
|
|
1086
|
+
timer = setTimeout(() => reject(new Error(`Grist did not answer grist.ready() within ${READY_TIMEOUT_MS / 1e3} s.`)), READY_TIMEOUT_MS);
|
|
1087
|
+
})
|
|
1088
|
+
]);
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
if (mine === attempt) update({ connection: "error", connectionError: err instanceof Error ? err.message : String(err) });
|
|
1091
|
+
return;
|
|
1092
|
+
} finally {
|
|
1093
|
+
clearTimeout(timer);
|
|
1094
|
+
}
|
|
1095
|
+
if (mine !== attempt) return;
|
|
1096
|
+
update({ connection: "ready" });
|
|
1097
|
+
await Promise.all([refreshMappings(), refreshTable()]);
|
|
1098
|
+
};
|
|
1099
|
+
const listen = () => {
|
|
1100
|
+
const offs = [
|
|
1101
|
+
// Copies: a host may reuse one record object and mutate it in place.
|
|
1102
|
+
recordStream.subscribe(
|
|
1103
|
+
({ record, mappings }) => update({
|
|
1104
|
+
record: record ? { ...record } : null,
|
|
1105
|
+
isNewRecord: false,
|
|
1106
|
+
...mappings ? { mappings } : {}
|
|
1107
|
+
})
|
|
1108
|
+
),
|
|
1109
|
+
recordsStream.subscribe(({ records, mappings }) => {
|
|
1110
|
+
update({ records: records.map((r) => ({ ...r })), ...mappings ? { mappings } : {} });
|
|
1111
|
+
void refreshTable();
|
|
1112
|
+
}),
|
|
1113
|
+
optionsStream.subscribe(
|
|
1114
|
+
({ options: widgetOptions, interaction }) => update({
|
|
1115
|
+
widgetOptions,
|
|
1116
|
+
interaction,
|
|
1117
|
+
grantedAccess: parseAccess(interaction?.access_level) ?? state.grantedAccess
|
|
1118
|
+
})
|
|
1119
|
+
),
|
|
1120
|
+
newRecordStream.subscribe(({ mappings }) => update({ isNewRecord: true, ...mappings ? { mappings } : {} })),
|
|
1121
|
+
messageStream.subscribe((msg) => {
|
|
1122
|
+
const theme = parseGristThemePayload(msg);
|
|
1123
|
+
if (theme) update({ theme });
|
|
1124
|
+
})
|
|
1125
|
+
];
|
|
1126
|
+
return () => offs.forEach((off) => off());
|
|
1127
|
+
};
|
|
1128
|
+
const begin = () => {
|
|
1129
|
+
if (!isEmbedded()) return update({ connection: "not-embedded" });
|
|
1130
|
+
if (!hasGrist()) {
|
|
1131
|
+
if (typeof document !== "undefined" && document.readyState !== "complete" && typeof window !== "undefined") {
|
|
1132
|
+
window.addEventListener("load", () => hasGrist() ? begin() : update({ connection: "unavailable" }), { once: true });
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
return update({ connection: "unavailable" });
|
|
1136
|
+
}
|
|
1137
|
+
stopStreams ?? (stopStreams = listen());
|
|
1138
|
+
void connect();
|
|
1139
|
+
};
|
|
1140
|
+
return {
|
|
1141
|
+
options,
|
|
1142
|
+
getState: () => state,
|
|
1143
|
+
subscribe(listener) {
|
|
1144
|
+
listeners.add(listener);
|
|
1145
|
+
return () => listeners.delete(listener);
|
|
1146
|
+
},
|
|
1147
|
+
start() {
|
|
1148
|
+
if (starts++ === 0) begin();
|
|
1149
|
+
return () => {
|
|
1150
|
+
if (--starts > 0) return;
|
|
1151
|
+
stopStreams?.();
|
|
1152
|
+
stopStreams = null;
|
|
1153
|
+
};
|
|
1154
|
+
},
|
|
1155
|
+
async reload() {
|
|
1156
|
+
if (!hasGrist() || !isEmbedded()) return begin();
|
|
1157
|
+
resetGristReady();
|
|
1158
|
+
update({ mappings: null });
|
|
1159
|
+
stopStreams ?? (stopStreams = listen());
|
|
1160
|
+
await connect();
|
|
1161
|
+
},
|
|
1162
|
+
update
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/sdk/use-grist.ts
|
|
1167
|
+
var GristContext = (0, import_react.createContext)(null);
|
|
1168
|
+
GristContext.displayName = "GristContext";
|
|
1169
|
+
function useGristRuntime() {
|
|
1170
|
+
return (0, import_react.useContext)(GristContext);
|
|
1171
|
+
}
|
|
1172
|
+
function newConnection(options) {
|
|
1173
|
+
const runtime = createGristRuntime(options);
|
|
1174
|
+
return { runtime, api: createGristApi(runtime) };
|
|
1175
|
+
}
|
|
1176
|
+
var pageConnection = null;
|
|
1177
|
+
function resetGristForTests() {
|
|
1178
|
+
pageConnection = null;
|
|
1179
|
+
resetGristReady();
|
|
1180
|
+
for (const s of ALL_STREAMS) s.reset();
|
|
1181
|
+
}
|
|
1182
|
+
var GristContextProvider = GristContext.Provider;
|
|
1183
|
+
var serverState = () => initialRuntimeState();
|
|
1184
|
+
function useGristState(connection) {
|
|
1185
|
+
const { runtime } = connection;
|
|
1186
|
+
(0, import_react.useEffect)(() => runtime.start(), [runtime]);
|
|
1187
|
+
return (0, import_react.useSyncExternalStore)(runtime.subscribe, runtime.getState, serverState);
|
|
1188
|
+
}
|
|
1189
|
+
var NO_ISSUES = Object.freeze([]);
|
|
1190
|
+
function useMapped(state, options) {
|
|
1191
|
+
const { record, records, mappings } = state;
|
|
1192
|
+
const { columns, schema } = options;
|
|
1193
|
+
return (0, import_react.useMemo)(() => {
|
|
1194
|
+
const issues = [];
|
|
1195
|
+
const rows = records && (columns?.length ? mappings !== null : true) ? records.map((r) => {
|
|
1196
|
+
const out = mapAndCheck(r, columns, mappings, schema);
|
|
1197
|
+
issues.push(...out.issues);
|
|
1198
|
+
return out.value;
|
|
1199
|
+
}) : null;
|
|
1200
|
+
const selected = mapAndCheck(record, columns, mappings, schema);
|
|
1201
|
+
if (record && !records?.some((r) => r.id === record.id)) issues.push(...selected.issues);
|
|
1202
|
+
const mappedRecords = rows && rows.every((r) => r !== null) ? rows : null;
|
|
1203
|
+
return { record: selected.value, records: mappedRecords, issues: issues.length ? issues : NO_ISSUES };
|
|
1204
|
+
}, [record, records, mappings, columns, schema]);
|
|
1205
|
+
}
|
|
1206
|
+
var EMPTY_MAPPINGS = Object.freeze({});
|
|
1207
|
+
var DETACHED = (() => {
|
|
1208
|
+
const state = initialRuntimeState();
|
|
1209
|
+
const runtime = {
|
|
1210
|
+
options: {},
|
|
1211
|
+
getState: () => state,
|
|
1212
|
+
subscribe: () => () => {
|
|
1213
|
+
},
|
|
1214
|
+
start: () => () => {
|
|
1215
|
+
},
|
|
1216
|
+
reload: async () => {
|
|
1217
|
+
},
|
|
1218
|
+
update: () => {
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
return { runtime, api: createGristApi(runtime) };
|
|
1222
|
+
})();
|
|
1223
|
+
function useNearestGristState() {
|
|
1224
|
+
const connection = (0, import_react.useContext)(GristContext);
|
|
1225
|
+
const state = useGristState(connection ?? DETACHED);
|
|
1226
|
+
const { issues } = useMapped(state, (connection ?? DETACHED).runtime.options);
|
|
1227
|
+
return { connection, state, cellIssues: issues };
|
|
1228
|
+
}
|
|
208
1229
|
|
|
209
1230
|
// src/emulator/api/command-api.ts
|
|
210
1231
|
function createCommandApiImpl(bus) {
|
|
@@ -252,36 +1273,7 @@ function createGristApiImpl() {
|
|
|
252
1273
|
};
|
|
253
1274
|
}
|
|
254
1275
|
|
|
255
|
-
// src/
|
|
256
|
-
function coerceGristDocApiRefRowId(value) {
|
|
257
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
258
|
-
if (!Array.isArray(value) || value.length < 2) return void 0;
|
|
259
|
-
const tag = value[0];
|
|
260
|
-
if (tag === "R" && typeof value[2] === "number" && Number.isFinite(value[2])) return value[2];
|
|
261
|
-
if (tag === "r" && typeof value[1] === "number" && Number.isFinite(value[1])) return value[1];
|
|
262
|
-
return void 0;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// src/sdk/helpers/grist-safe-parse.ts
|
|
266
|
-
var import_zod = require("zod");
|
|
267
|
-
var gristSafeRefValueSchema = import_zod.z.object({
|
|
268
|
-
rowId: import_zod.z.number(),
|
|
269
|
-
display: import_zod.z.string().nullable().optional()
|
|
270
|
-
});
|
|
271
|
-
var gristSafeRefListValueSchema = import_zod.z.object({
|
|
272
|
-
rowIds: import_zod.z.array(import_zod.z.number()),
|
|
273
|
-
displays: import_zod.z.array(import_zod.z.string()).optional()
|
|
274
|
-
});
|
|
275
|
-
var gristCellValueSchema = import_zod.z.union([
|
|
276
|
-
import_zod.z.null(),
|
|
277
|
-
import_zod.z.string(),
|
|
278
|
-
import_zod.z.number(),
|
|
279
|
-
import_zod.z.boolean(),
|
|
280
|
-
import_zod.z.date(),
|
|
281
|
-
import_zod.z.array(import_zod.z.unknown()),
|
|
282
|
-
gristSafeRefValueSchema,
|
|
283
|
-
gristSafeRefListValueSchema
|
|
284
|
-
]);
|
|
1276
|
+
// src/emulator/core/display-helpers.ts
|
|
285
1277
|
function inferDisplayHelpersByRef(columns) {
|
|
286
1278
|
const out = {};
|
|
287
1279
|
const helperColumns = Object.entries(columns).filter(
|
|
@@ -482,7 +1474,7 @@ function evaluateRowFormulas(state, table, row) {
|
|
|
482
1474
|
try {
|
|
483
1475
|
row[colId] = col.formulaFn(row, { table, document: state });
|
|
484
1476
|
} catch (err) {
|
|
485
|
-
row[colId] =
|
|
1477
|
+
row[colId] = formulaError2(err);
|
|
486
1478
|
}
|
|
487
1479
|
}
|
|
488
1480
|
}
|
|
@@ -569,9 +1561,9 @@ function syncRefListDisplayHelpers(state, table, row) {
|
|
|
569
1561
|
row[helperColId] = labels;
|
|
570
1562
|
}
|
|
571
1563
|
}
|
|
572
|
-
function
|
|
573
|
-
const
|
|
574
|
-
return ["E", "Error",
|
|
1564
|
+
function formulaError2(err) {
|
|
1565
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
1566
|
+
return ["E", "Error", message2];
|
|
575
1567
|
}
|
|
576
1568
|
function synthesizeTablesMeta(state) {
|
|
577
1569
|
const ids = [];
|
|
@@ -653,8 +1645,8 @@ function synthesizeTablesColumnMeta(state) {
|
|
|
653
1645
|
|
|
654
1646
|
// src/emulator/core/action-engine.ts
|
|
655
1647
|
var EmulatorActionError = class extends Error {
|
|
656
|
-
constructor(action,
|
|
657
|
-
super(
|
|
1648
|
+
constructor(action, message2) {
|
|
1649
|
+
super(message2);
|
|
658
1650
|
this.action = action;
|
|
659
1651
|
this.name = "EmulatorActionError";
|
|
660
1652
|
}
|
|
@@ -955,8 +1947,8 @@ function normalizeRefListValue(value) {
|
|
|
955
1947
|
return ["L", ...toIdList(value)];
|
|
956
1948
|
}
|
|
957
1949
|
if (typeof value === "string") {
|
|
958
|
-
const
|
|
959
|
-
return ["L", ...toIdList(
|
|
1950
|
+
const tokens2 = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1951
|
+
return ["L", ...toIdList(tokens2)];
|
|
960
1952
|
}
|
|
961
1953
|
if (value && typeof value === "object" && Array.isArray(value.rowIds)) {
|
|
962
1954
|
return ["L", ...toIdList(value.rowIds)];
|
|
@@ -1094,8 +2086,9 @@ function createWidgetApiImpl(state, bus) {
|
|
|
1094
2086
|
async getOptions() {
|
|
1095
2087
|
return Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
|
|
1096
2088
|
},
|
|
2089
|
+
// Replaces every option, as Grist's WidgetAPI.setOptions does.
|
|
1097
2090
|
async setOptions(options) {
|
|
1098
|
-
state.widgetOptions = { ...
|
|
2091
|
+
state.widgetOptions = { ...options };
|
|
1099
2092
|
emit();
|
|
1100
2093
|
},
|
|
1101
2094
|
async clearOptions() {
|
|
@@ -1161,157 +2154,6 @@ function buildGristThemeHostMessage(appearance, options) {
|
|
|
1161
2154
|
};
|
|
1162
2155
|
}
|
|
1163
2156
|
|
|
1164
|
-
// src/emulator/transports/iframe.ts
|
|
1165
|
-
var import_grain_rpc = require("grain-rpc");
|
|
1166
|
-
function createIframeTransport(options) {
|
|
1167
|
-
const { iframe, state, bus, api, targetOrigin = "*" } = options;
|
|
1168
|
-
const timers = /* @__PURE__ */ new Set();
|
|
1169
|
-
const rpc = new import_grain_rpc.Rpc({
|
|
1170
|
-
sendMessage: (msg) => {
|
|
1171
|
-
const target = iframe.contentWindow;
|
|
1172
|
-
if (!target) return;
|
|
1173
|
-
target.postMessage(msg, targetOrigin);
|
|
1174
|
-
},
|
|
1175
|
-
logger: { info: () => {
|
|
1176
|
-
}, warn: () => {
|
|
1177
|
-
} }
|
|
1178
|
-
});
|
|
1179
|
-
rpc.queueOutgoingUntilReadyMessage();
|
|
1180
|
-
const receiveFromWidget = rpc.receiveMessage.bind(rpc);
|
|
1181
|
-
rpc.receiveMessage = (msg) => {
|
|
1182
|
-
if (msg && typeof msg === "object" && msg.mtype === import_grain_rpc.MsgType.RpcCall && msg.mdest === "grist") {
|
|
1183
|
-
const copy = { ...msg };
|
|
1184
|
-
delete copy.mdest;
|
|
1185
|
-
receiveFromWidget(copy);
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
receiveFromWidget(msg);
|
|
1189
|
-
};
|
|
1190
|
-
const onMessage = (event) => {
|
|
1191
|
-
if (event.source !== iframe.contentWindow) return;
|
|
1192
|
-
if (!isRpcEnvelope(event.data)) return;
|
|
1193
|
-
rpc.receiveMessage(event.data);
|
|
1194
|
-
};
|
|
1195
|
-
window.addEventListener("message", onMessage);
|
|
1196
|
-
rpc.registerImpl("GristView", api.GristView);
|
|
1197
|
-
rpc.registerImpl("GristDocAPI", api.GristDocAPI);
|
|
1198
|
-
rpc.registerImpl("WidgetAPI", api.WidgetAPI);
|
|
1199
|
-
rpc.registerImpl("CustomSectionAPI", api.CustomSectionAPI);
|
|
1200
|
-
rpc.registerImpl("_grist_api", api.GristAPI);
|
|
1201
|
-
rpc.registerImpl("CommandAPI", api.CommandAPI);
|
|
1202
|
-
const buildInteraction = options.buildInteraction ?? defaultInteraction(state);
|
|
1203
|
-
const cursorOf = options.cursor ?? (() => state.selection.cursor);
|
|
1204
|
-
const initialCursorOf = options.initialCursor ?? (() => state.selection.rowId);
|
|
1205
|
-
function fireDataChange(tableId, opts) {
|
|
1206
|
-
safePostMessage({
|
|
1207
|
-
tableId: tableId ?? state.selection.tableId,
|
|
1208
|
-
dataChange: true,
|
|
1209
|
-
...opts?.mappingsChange ? { mappingsChange: true } : {}
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
function fireCursor(rowId, opts) {
|
|
1213
|
-
const id = rowId === void 0 ? initialCursorOf() : rowId;
|
|
1214
|
-
if (id === null) return;
|
|
1215
|
-
safePostMessage({
|
|
1216
|
-
tableId: state.selection.tableId,
|
|
1217
|
-
rowId: id,
|
|
1218
|
-
cursor: cursorOf(),
|
|
1219
|
-
...opts?.mappingsChange ? { mappingsChange: true } : {}
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
function fireOptions() {
|
|
1223
|
-
safePostMessage({
|
|
1224
|
-
options: hasOptions(state.widgetOptions) ? { ...state.widgetOptions } : null,
|
|
1225
|
-
settings: { ...buildInteraction() }
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
function fireTheme(message) {
|
|
1229
|
-
safePostMessage(message);
|
|
1230
|
-
}
|
|
1231
|
-
function emitInitialState(opts) {
|
|
1232
|
-
fireOptions();
|
|
1233
|
-
if (options.initialTheme) {
|
|
1234
|
-
fireTheme(buildGristThemeHostMessage(options.initialTheme, { fromReady: true }));
|
|
1235
|
-
}
|
|
1236
|
-
fireDataChange(void 0, opts);
|
|
1237
|
-
fireCursor(void 0, opts);
|
|
1238
|
-
}
|
|
1239
|
-
function emitInitialStateWithReplay(opts) {
|
|
1240
|
-
emitInitialState(opts);
|
|
1241
|
-
queueMicrotask(() => emitInitialState(opts));
|
|
1242
|
-
const timer = setTimeout(() => {
|
|
1243
|
-
timers.delete(timer);
|
|
1244
|
-
emitInitialState(opts);
|
|
1245
|
-
}, 0);
|
|
1246
|
-
timers.add(timer);
|
|
1247
|
-
}
|
|
1248
|
-
const offBus = bus.on((event) => {
|
|
1249
|
-
switch (event.type) {
|
|
1250
|
-
case "ready":
|
|
1251
|
-
emitInitialStateWithReplay();
|
|
1252
|
-
break;
|
|
1253
|
-
case "data-change":
|
|
1254
|
-
fireDataChange(event.tableId);
|
|
1255
|
-
break;
|
|
1256
|
-
case "cursor-change": {
|
|
1257
|
-
const rowId = event.rowId === null ? state.selection.rowId : event.rowId;
|
|
1258
|
-
if (rowId !== null) {
|
|
1259
|
-
safePostMessage({
|
|
1260
|
-
tableId: event.tableId,
|
|
1261
|
-
rowId,
|
|
1262
|
-
cursor: cursorOf()
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
fireDataChange(event.tableId);
|
|
1266
|
-
break;
|
|
1267
|
-
}
|
|
1268
|
-
case "options-change":
|
|
1269
|
-
fireOptions();
|
|
1270
|
-
break;
|
|
1271
|
-
case "mappings-change":
|
|
1272
|
-
emitInitialStateWithReplay({ mappingsChange: true });
|
|
1273
|
-
break;
|
|
1274
|
-
case "theme-change":
|
|
1275
|
-
fireTheme(event.message);
|
|
1276
|
-
break;
|
|
1277
|
-
}
|
|
1278
|
-
});
|
|
1279
|
-
rpc.processIncoming();
|
|
1280
|
-
function safePostMessage(payload) {
|
|
1281
|
-
try {
|
|
1282
|
-
rpc.postMessage(payload);
|
|
1283
|
-
} catch (err) {
|
|
1284
|
-
console.warn("[grist-emulator/iframe] failed to deliver postMessage", err);
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
return {
|
|
1288
|
-
dispose() {
|
|
1289
|
-
window.removeEventListener("message", onMessage);
|
|
1290
|
-
offBus();
|
|
1291
|
-
for (const timer of timers) clearTimeout(timer);
|
|
1292
|
-
timers.clear();
|
|
1293
|
-
},
|
|
1294
|
-
fireDataChange,
|
|
1295
|
-
fireCursor,
|
|
1296
|
-
fireOptions,
|
|
1297
|
-
fireTheme,
|
|
1298
|
-
emitInitialState
|
|
1299
|
-
};
|
|
1300
|
-
}
|
|
1301
|
-
function hasOptions(options) {
|
|
1302
|
-
return Object.keys(options).length > 0;
|
|
1303
|
-
}
|
|
1304
|
-
function isRpcEnvelope(data) {
|
|
1305
|
-
if (!data || typeof data !== "object") return false;
|
|
1306
|
-
return typeof data.mtype === "number";
|
|
1307
|
-
}
|
|
1308
|
-
function defaultInteraction(state) {
|
|
1309
|
-
return () => ({
|
|
1310
|
-
access_level: state.configuration?.requiredAccess ?? "read table",
|
|
1311
|
-
...state.widgetInteraction
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
2157
|
// src/sdk/helpers/grist-user-actions.ts
|
|
1316
2158
|
function gristUpdateRecordAction(tableId, rowId, patch) {
|
|
1317
2159
|
return ["UpdateRecord", tableId, rowId, patch];
|
|
@@ -1367,14 +2209,6 @@ function createInlineTransport(options) {
|
|
|
1367
2209
|
logErr(err);
|
|
1368
2210
|
}
|
|
1369
2211
|
}
|
|
1370
|
-
lastRecord = { record: null, mappings };
|
|
1371
|
-
for (const [cb] of recordCbs) {
|
|
1372
|
-
try {
|
|
1373
|
-
cb(null, mappings);
|
|
1374
|
-
} catch (err) {
|
|
1375
|
-
logErr(err);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
2212
|
return;
|
|
1379
2213
|
}
|
|
1380
2214
|
if (rowId === null) {
|
|
@@ -1669,8 +2503,8 @@ function createInlineTransport(options) {
|
|
|
1669
2503
|
fireOptions() {
|
|
1670
2504
|
pushOptions();
|
|
1671
2505
|
},
|
|
1672
|
-
fireTheme(
|
|
1673
|
-
bus.emit({ type: "theme-change", message });
|
|
2506
|
+
fireTheme(message2) {
|
|
2507
|
+
bus.emit({ type: "theme-change", message: message2 });
|
|
1674
2508
|
},
|
|
1675
2509
|
emitInitialState() {
|
|
1676
2510
|
pushOptions();
|
|
@@ -1713,57 +2547,57 @@ function installFakeIframeContext(target) {
|
|
|
1713
2547
|
}
|
|
1714
2548
|
|
|
1715
2549
|
// src/emulator/validators.ts
|
|
1716
|
-
var
|
|
1717
|
-
var emulatorRowSchema =
|
|
1718
|
-
var formulaKindSchema =
|
|
1719
|
-
var replicaColumnSchema =
|
|
1720
|
-
colRef:
|
|
1721
|
-
type:
|
|
1722
|
-
label:
|
|
1723
|
-
parentPos:
|
|
1724
|
-
isFormula:
|
|
2550
|
+
var import_zod = require("zod");
|
|
2551
|
+
var emulatorRowSchema = import_zod.z.object({ id: import_zod.z.number() }).catchall(import_zod.z.unknown());
|
|
2552
|
+
var formulaKindSchema = import_zod.z.enum(["computed", "trigger", "none", "empty_slot"]);
|
|
2553
|
+
var replicaColumnSchema = import_zod.z.object({
|
|
2554
|
+
colRef: import_zod.z.number().optional(),
|
|
2555
|
+
type: import_zod.z.string().optional(),
|
|
2556
|
+
label: import_zod.z.string().optional(),
|
|
2557
|
+
parentPos: import_zod.z.number().optional(),
|
|
2558
|
+
isFormula: import_zod.z.boolean().optional(),
|
|
1725
2559
|
formulaKind: formulaKindSchema.optional(),
|
|
1726
|
-
formula:
|
|
1727
|
-
description:
|
|
1728
|
-
widgetOptions:
|
|
1729
|
-
displayColRef:
|
|
1730
|
-
visibleColRef:
|
|
2560
|
+
formula: import_zod.z.string().optional(),
|
|
2561
|
+
description: import_zod.z.string().optional(),
|
|
2562
|
+
widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
|
|
2563
|
+
displayColRef: import_zod.z.number().optional(),
|
|
2564
|
+
visibleColRef: import_zod.z.number().optional()
|
|
1731
2565
|
}).strict();
|
|
1732
|
-
var replicaTableSchema =
|
|
1733
|
-
label:
|
|
1734
|
-
columns:
|
|
1735
|
-
rows:
|
|
1736
|
-
rowCount:
|
|
1737
|
-
dataOmitted:
|
|
1738
|
-
dataOmittedReason:
|
|
1739
|
-
error:
|
|
2566
|
+
var replicaTableSchema = import_zod.z.object({
|
|
2567
|
+
label: import_zod.z.string().optional(),
|
|
2568
|
+
columns: import_zod.z.record(import_zod.z.string(), replicaColumnSchema).optional(),
|
|
2569
|
+
rows: import_zod.z.array(emulatorRowSchema).optional(),
|
|
2570
|
+
rowCount: import_zod.z.number().optional(),
|
|
2571
|
+
dataOmitted: import_zod.z.boolean().optional(),
|
|
2572
|
+
dataOmittedReason: import_zod.z.string().optional(),
|
|
2573
|
+
error: import_zod.z.string().optional()
|
|
1740
2574
|
}).strict();
|
|
1741
|
-
var replicaSelectionSchema =
|
|
1742
|
-
tableId:
|
|
1743
|
-
rowId:
|
|
1744
|
-
mappings:
|
|
1745
|
-
cursor:
|
|
2575
|
+
var replicaSelectionSchema = import_zod.z.object({
|
|
2576
|
+
tableId: import_zod.z.string().optional(),
|
|
2577
|
+
rowId: import_zod.z.union([import_zod.z.number(), import_zod.z.literal("new"), import_zod.z.null()]).optional(),
|
|
2578
|
+
mappings: import_zod.z.unknown().nullable().optional(),
|
|
2579
|
+
cursor: import_zod.z.unknown().optional()
|
|
1746
2580
|
}).strict();
|
|
1747
|
-
var replicaWidgetConfigurationSchema =
|
|
1748
|
-
requiredAccess:
|
|
1749
|
-
columns:
|
|
1750
|
-
allowSelectBy:
|
|
1751
|
-
hasCustomOptions:
|
|
2581
|
+
var replicaWidgetConfigurationSchema = import_zod.z.object({
|
|
2582
|
+
requiredAccess: import_zod.z.string().optional(),
|
|
2583
|
+
columns: import_zod.z.unknown().optional(),
|
|
2584
|
+
allowSelectBy: import_zod.z.boolean().optional(),
|
|
2585
|
+
hasCustomOptions: import_zod.z.boolean().optional()
|
|
1752
2586
|
}).strict();
|
|
1753
|
-
var replicaAccessTokenSchema =
|
|
1754
|
-
token:
|
|
1755
|
-
baseUrl:
|
|
1756
|
-
ttlMsecs:
|
|
2587
|
+
var replicaAccessTokenSchema = import_zod.z.object({
|
|
2588
|
+
token: import_zod.z.string(),
|
|
2589
|
+
baseUrl: import_zod.z.string(),
|
|
2590
|
+
ttlMsecs: import_zod.z.number()
|
|
1757
2591
|
}).strict();
|
|
1758
|
-
var replicaDocumentSchema =
|
|
1759
|
-
generatedAt:
|
|
1760
|
-
source:
|
|
1761
|
-
docName:
|
|
1762
|
-
mode:
|
|
1763
|
-
tables:
|
|
2592
|
+
var replicaDocumentSchema = import_zod.z.object({
|
|
2593
|
+
generatedAt: import_zod.z.string(),
|
|
2594
|
+
source: import_zod.z.string().optional(),
|
|
2595
|
+
docName: import_zod.z.string().optional(),
|
|
2596
|
+
mode: import_zod.z.enum(["schema-only", "schema+samples", "schema+data"]).optional(),
|
|
2597
|
+
tables: import_zod.z.record(import_zod.z.string(), replicaTableSchema),
|
|
1764
2598
|
selection: replicaSelectionSchema.optional(),
|
|
1765
|
-
widgetOptions:
|
|
1766
|
-
widgetInteraction:
|
|
2599
|
+
widgetOptions: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
|
|
2600
|
+
widgetInteraction: import_zod.z.unknown().optional(),
|
|
1767
2601
|
configuration: replicaWidgetConfigurationSchema.optional(),
|
|
1768
2602
|
accessToken: replicaAccessTokenSchema.nullable().optional()
|
|
1769
2603
|
}).strict().refine((value) => Object.keys(value.tables).length > 0, {
|
|
@@ -1794,34 +2628,21 @@ function createGristEmulator(options) {
|
|
|
1794
2628
|
const state = createDocument({
|
|
1795
2629
|
...validatedDocument,
|
|
1796
2630
|
widgetOptions: options.widgetOptions ?? validatedDocument.widgetOptions,
|
|
1797
|
-
|
|
2631
|
+
// No access_level by default: the emulator then grants the access the
|
|
2632
|
+
// widget asks for in grist.ready(), as a user accepting it in Grist would.
|
|
2633
|
+
widgetInteraction: options.widgetInteraction ?? validatedDocument.widgetInteraction ?? {}
|
|
1798
2634
|
});
|
|
1799
2635
|
const bus = createEventBus();
|
|
1800
2636
|
const api = createApiBundle(state, bus);
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
transport
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
bus,
|
|
1811
|
-
api,
|
|
1812
|
-
initialTheme: options.initialTheme
|
|
1813
|
-
});
|
|
1814
|
-
} else {
|
|
1815
|
-
const inline = createInlineTransport({
|
|
1816
|
-
state,
|
|
1817
|
-
bus,
|
|
1818
|
-
api,
|
|
1819
|
-
initialTheme: options.initialTheme,
|
|
1820
|
-
target: options.transport?.kind === "inline" ? options.transport.target : void 0
|
|
1821
|
-
});
|
|
1822
|
-
transport = inline;
|
|
1823
|
-
grist2 = inline.grist;
|
|
1824
|
-
}
|
|
2637
|
+
const inline = createInlineTransport({
|
|
2638
|
+
state,
|
|
2639
|
+
bus,
|
|
2640
|
+
api,
|
|
2641
|
+
initialTheme: options.initialTheme,
|
|
2642
|
+
target: options.transport?.target
|
|
2643
|
+
});
|
|
2644
|
+
const transport = inline;
|
|
2645
|
+
const grist2 = inline.grist;
|
|
1825
2646
|
function apply(actions) {
|
|
1826
2647
|
return applyUserActions(state, bus, actions);
|
|
1827
2648
|
}
|
|
@@ -1963,18 +2784,14 @@ var presets = {
|
|
|
1963
2784
|
// src/emulator/testing/render-with-grist.tsx
|
|
1964
2785
|
function renderWithGrist(ui, options = {}) {
|
|
1965
2786
|
fakeIframeContext();
|
|
1966
|
-
|
|
1967
|
-
resetGristReadyCacheForTests();
|
|
1968
|
-
resetEmbeddedHandshakeManagerForTests();
|
|
2787
|
+
resetGristForTests();
|
|
1969
2788
|
const emulator = options.existing ?? createGristEmulator(options.emulator ?? { document: presets.blank() });
|
|
1970
|
-
const result = (0,
|
|
2789
|
+
const result = (0, import_react2.render)(ui, options);
|
|
1971
2790
|
const originalUnmount = result.unmount;
|
|
1972
2791
|
result.unmount = () => {
|
|
1973
2792
|
originalUnmount();
|
|
1974
2793
|
if (!options.existing) emulator.dispose();
|
|
1975
|
-
|
|
1976
|
-
resetGristEventsForTests();
|
|
1977
|
-
resetEmbeddedHandshakeManagerForTests();
|
|
2794
|
+
resetGristForTests();
|
|
1978
2795
|
};
|
|
1979
2796
|
return Object.assign(result, { emulator });
|
|
1980
2797
|
}
|
|
@@ -1990,6 +2807,880 @@ function fakeIframeContext() {
|
|
|
1990
2807
|
}
|
|
1991
2808
|
}
|
|
1992
2809
|
|
|
2810
|
+
// src/emulator/testing/smoke-test-widget.tsx
|
|
2811
|
+
var import_react9 = require("react");
|
|
2812
|
+
var import_react10 = require("@testing-library/react");
|
|
2813
|
+
|
|
2814
|
+
// src/app/mount-grist-widget.tsx
|
|
2815
|
+
var import_react8 = require("react");
|
|
2816
|
+
var import_client = require("react-dom/client");
|
|
2817
|
+
|
|
2818
|
+
// src/sdk/components/grist-widget-provider.tsx
|
|
2819
|
+
var import_react3 = require("react");
|
|
2820
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
2821
|
+
function GristWidgetProvider({ options, children }) {
|
|
2822
|
+
const [connection] = (0, import_react3.useState)(() => newConnection(options));
|
|
2823
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GristContextProvider, { value: connection, children });
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
// src/sdk/components/grist-boundary.tsx
|
|
2827
|
+
var import_react4 = require("react");
|
|
2828
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
2829
|
+
var shellStyle = {
|
|
2830
|
+
alignItems: "center",
|
|
2831
|
+
background: "var(--background)",
|
|
2832
|
+
boxSizing: "border-box",
|
|
2833
|
+
color: "var(--foreground)",
|
|
2834
|
+
display: "flex",
|
|
2835
|
+
flexDirection: "column",
|
|
2836
|
+
justifyContent: "center",
|
|
2837
|
+
minHeight: "100%",
|
|
2838
|
+
padding: "clamp(16px, 5vh, 32px)",
|
|
2839
|
+
width: "100%"
|
|
2840
|
+
};
|
|
2841
|
+
var cardStyle = {
|
|
2842
|
+
background: "var(--card)",
|
|
2843
|
+
border: "1px solid var(--border)",
|
|
2844
|
+
borderRadius: 12,
|
|
2845
|
+
boxShadow: "0 1px 3px rgb(0 0 0 / 0.12)",
|
|
2846
|
+
maxWidth: 420,
|
|
2847
|
+
padding: "20px 22px",
|
|
2848
|
+
textAlign: "center",
|
|
2849
|
+
width: "100%"
|
|
2850
|
+
};
|
|
2851
|
+
var titleStyle = {
|
|
2852
|
+
color: "var(--foreground)",
|
|
2853
|
+
fontSize: 15,
|
|
2854
|
+
fontWeight: 600,
|
|
2855
|
+
lineHeight: 1.35,
|
|
2856
|
+
margin: 0
|
|
2857
|
+
};
|
|
2858
|
+
var messageStyle = {
|
|
2859
|
+
color: "var(--muted-foreground)",
|
|
2860
|
+
fontSize: 14,
|
|
2861
|
+
lineHeight: 1.5,
|
|
2862
|
+
margin: "10px 0 0"
|
|
2863
|
+
};
|
|
2864
|
+
var buttonStyle = {
|
|
2865
|
+
background: "var(--background)",
|
|
2866
|
+
border: "1px solid var(--border)",
|
|
2867
|
+
borderRadius: 8,
|
|
2868
|
+
color: "var(--foreground)",
|
|
2869
|
+
cursor: "pointer",
|
|
2870
|
+
fontSize: 13,
|
|
2871
|
+
fontWeight: 500,
|
|
2872
|
+
marginTop: 16,
|
|
2873
|
+
padding: "8px 14px"
|
|
2874
|
+
};
|
|
2875
|
+
function GristBoundaryScreen({
|
|
2876
|
+
title,
|
|
2877
|
+
message: message2,
|
|
2878
|
+
tone = "default",
|
|
2879
|
+
showRetry = false,
|
|
2880
|
+
actions,
|
|
2881
|
+
retryLabel = "Try reconnecting",
|
|
2882
|
+
retryBusyLabel = "Reconnecting\u2026"
|
|
2883
|
+
}) {
|
|
2884
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: shellStyle, role: tone === "error" ? "alert" : "status", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: cardStyle, children: [
|
|
2885
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { style: titleStyle, children: title }),
|
|
2886
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: messageStyle, children: message2 }),
|
|
2887
|
+
showRetry && actions ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2888
|
+
"button",
|
|
2889
|
+
{
|
|
2890
|
+
disabled: actions.isReloading,
|
|
2891
|
+
onClick: () => void actions.reload(),
|
|
2892
|
+
style: {
|
|
2893
|
+
...buttonStyle,
|
|
2894
|
+
cursor: actions.isReloading ? "not-allowed" : "pointer",
|
|
2895
|
+
opacity: actions.isReloading ? 0.7 : 1
|
|
2896
|
+
},
|
|
2897
|
+
type: "button",
|
|
2898
|
+
children: actions.isReloading ? retryBusyLabel : retryLabel
|
|
2899
|
+
}
|
|
2900
|
+
) : null
|
|
2901
|
+
] }) });
|
|
2902
|
+
}
|
|
2903
|
+
function boundaryScreen(state, columns) {
|
|
2904
|
+
switch (state.connection) {
|
|
2905
|
+
case "connecting":
|
|
2906
|
+
return { title: "Connecting to Grist", message: "Asking Grist for the access this widget needs\u2026" };
|
|
2907
|
+
case "not-embedded":
|
|
2908
|
+
return {
|
|
2909
|
+
title: "Open inside Grist",
|
|
2910
|
+
message: "Open this page as a custom widget inside a Grist document (it runs in an embedded panel, not on its own).",
|
|
2911
|
+
retry: true
|
|
2912
|
+
};
|
|
2913
|
+
case "unavailable":
|
|
2914
|
+
return {
|
|
2915
|
+
title: "Open inside Grist",
|
|
2916
|
+
message: "The Grist plugin did not load. Reload the page, or open this URL as a custom widget inside a Grist document.",
|
|
2917
|
+
retry: true
|
|
2918
|
+
};
|
|
2919
|
+
case "error":
|
|
2920
|
+
return {
|
|
2921
|
+
title: "Something went wrong",
|
|
2922
|
+
message: state.connectionError ?? "Something went wrong connecting to Grist. Try again, or reload the page.",
|
|
2923
|
+
tone: "error",
|
|
2924
|
+
retry: true
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
if (accessDenied(state)) {
|
|
2928
|
+
return {
|
|
2929
|
+
title: "More access needed",
|
|
2930
|
+
message: `Set Access level to at least "${accessLabel(state.declaredAccess)}" in the Custom widget settings.`,
|
|
2931
|
+
tone: "error",
|
|
2932
|
+
retry: true
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
const mapping = deriveColumnMappingStatus(columns, state.mappings);
|
|
2936
|
+
if (mapping.pending) return { title: "Connecting to Grist", message: "Waiting for Grist to report the column mapping\u2026" };
|
|
2937
|
+
if (!mapping.ok) {
|
|
2938
|
+
return {
|
|
2939
|
+
title: "Map your columns",
|
|
2940
|
+
message: "Open the widget settings in Grist (Creator Panel \u2192 Custom widget) and connect each required column to your table."
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
return null;
|
|
2944
|
+
}
|
|
2945
|
+
function GristBoundary({ children }) {
|
|
2946
|
+
const connection = useGristRuntime();
|
|
2947
|
+
if (!connection) throw new Error("<GristBoundary> must be used inside <GristWidgetProvider> (mountGristWidget renders both).");
|
|
2948
|
+
const state = useGristState(connection);
|
|
2949
|
+
const [isReloading, setIsReloading] = (0, import_react4.useState)(false);
|
|
2950
|
+
const reload = (0, import_react4.useCallback)(async () => {
|
|
2951
|
+
setIsReloading(true);
|
|
2952
|
+
try {
|
|
2953
|
+
await connection.runtime.reload();
|
|
2954
|
+
} catch (err) {
|
|
2955
|
+
console.error("[grist-widget-sdk] reload failed:", err);
|
|
2956
|
+
} finally {
|
|
2957
|
+
setIsReloading(false);
|
|
2958
|
+
}
|
|
2959
|
+
}, [connection]);
|
|
2960
|
+
const screen2 = boundaryScreen(state, connection.runtime.options.columns);
|
|
2961
|
+
if (!screen2) return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
|
|
2962
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2963
|
+
GristBoundaryScreen,
|
|
2964
|
+
{
|
|
2965
|
+
title: screen2.title,
|
|
2966
|
+
message: screen2.message,
|
|
2967
|
+
tone: screen2.tone,
|
|
2968
|
+
showRetry: screen2.retry,
|
|
2969
|
+
actions: { reload, isReloading },
|
|
2970
|
+
retryLabel: screen2.tone === "error" ? "Try again" : "Try reconnecting",
|
|
2971
|
+
retryBusyLabel: screen2.tone === "error" ? "Retrying\u2026" : "Reconnecting\u2026"
|
|
2972
|
+
}
|
|
2973
|
+
);
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// src/app/chrome/tokens.ts
|
|
2977
|
+
var token = (name, fallback) => `var(--${name}, ${fallback})`;
|
|
2978
|
+
var tokens = {
|
|
2979
|
+
background: token("background", "#ffffff"),
|
|
2980
|
+
foreground: token("foreground", "#0a0a0a"),
|
|
2981
|
+
card: token("card", "#ffffff"),
|
|
2982
|
+
mutedForeground: token("muted-foreground", "#737373"),
|
|
2983
|
+
border: token("border", "#e5e5e5"),
|
|
2984
|
+
primary: token("primary", "#171717"),
|
|
2985
|
+
primaryForeground: token("primary-foreground", "#fafafa"),
|
|
2986
|
+
destructive: token("destructive", "#dc2626"),
|
|
2987
|
+
radius: token("radius", "0.625rem")
|
|
2988
|
+
};
|
|
2989
|
+
var pageStyle = {
|
|
2990
|
+
boxSizing: "border-box",
|
|
2991
|
+
display: "flex",
|
|
2992
|
+
flexDirection: "column",
|
|
2993
|
+
justifyContent: "center",
|
|
2994
|
+
maxWidth: "28rem",
|
|
2995
|
+
minHeight: "100%",
|
|
2996
|
+
margin: "0 auto",
|
|
2997
|
+
padding: "4rem 1.5rem",
|
|
2998
|
+
color: tokens.foreground,
|
|
2999
|
+
font: "inherit"
|
|
3000
|
+
};
|
|
3001
|
+
var eyebrowStyle = {
|
|
3002
|
+
margin: "0 0 0.5rem",
|
|
3003
|
+
textAlign: "center",
|
|
3004
|
+
fontSize: "0.875rem",
|
|
3005
|
+
fontWeight: 500,
|
|
3006
|
+
color: tokens.mutedForeground
|
|
3007
|
+
};
|
|
3008
|
+
var headingStyle = {
|
|
3009
|
+
margin: 0,
|
|
3010
|
+
textAlign: "center",
|
|
3011
|
+
fontSize: "1.5rem",
|
|
3012
|
+
fontWeight: 600,
|
|
3013
|
+
letterSpacing: "-0.01em"
|
|
3014
|
+
};
|
|
3015
|
+
var leadStyle = {
|
|
3016
|
+
margin: "0.75rem auto 2rem",
|
|
3017
|
+
maxWidth: "24rem",
|
|
3018
|
+
textAlign: "center",
|
|
3019
|
+
fontSize: "0.875rem",
|
|
3020
|
+
lineHeight: 1.5,
|
|
3021
|
+
color: tokens.mutedForeground
|
|
3022
|
+
};
|
|
3023
|
+
var cardStyle2 = {
|
|
3024
|
+
boxSizing: "border-box",
|
|
3025
|
+
marginBottom: "1rem",
|
|
3026
|
+
padding: "1.25rem",
|
|
3027
|
+
border: `1px solid ${tokens.border}`,
|
|
3028
|
+
borderRadius: tokens.radius,
|
|
3029
|
+
background: tokens.card
|
|
3030
|
+
};
|
|
3031
|
+
var cardTitleStyle = {
|
|
3032
|
+
margin: "0 0 0.375rem",
|
|
3033
|
+
fontSize: "1rem",
|
|
3034
|
+
fontWeight: 600
|
|
3035
|
+
};
|
|
3036
|
+
var cardTextStyle = {
|
|
3037
|
+
margin: 0,
|
|
3038
|
+
fontSize: "0.875rem",
|
|
3039
|
+
lineHeight: 1.5,
|
|
3040
|
+
color: tokens.mutedForeground
|
|
3041
|
+
};
|
|
3042
|
+
var codeStyle = {
|
|
3043
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
3044
|
+
fontSize: "0.8125rem",
|
|
3045
|
+
color: tokens.foreground
|
|
3046
|
+
};
|
|
3047
|
+
|
|
3048
|
+
// src/app/chrome/widget-title.ts
|
|
3049
|
+
function widgetTitle() {
|
|
3050
|
+
const title = typeof document === "undefined" ? "" : document.title.trim();
|
|
3051
|
+
return title || "Grist widget";
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// src/app/chrome/local-dev-page.tsx
|
|
3055
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
3056
|
+
function LocalDevPage() {
|
|
3057
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("main", { style: pageStyle, children: [
|
|
3058
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: eyebrowStyle, children: [
|
|
3059
|
+
widgetTitle(),
|
|
3060
|
+
" \xB7 local dev"
|
|
3061
|
+
] }),
|
|
3062
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { style: headingStyle, children: "Not embedded in Grist" }),
|
|
3063
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: leadStyle, children: "This is the local dev server: there's no Grist here to render the widget against." }),
|
|
3064
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: cardStyle2, children: [
|
|
3065
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: cardTitleStyle, children: "Check it against real Grist" }),
|
|
3066
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: cardTextStyle, children: [
|
|
3067
|
+
"Push to ",
|
|
3068
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "dev" }),
|
|
3069
|
+
", then open its ",
|
|
3070
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "/dev/" }),
|
|
3071
|
+
" URL in a custom widget of a real Grist document: later pushes reload it there within seconds. Locally,",
|
|
3072
|
+
" ",
|
|
3073
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "pnpm test" }),
|
|
3074
|
+
" runs the widget against an emulated Grist."
|
|
3075
|
+
] })
|
|
3076
|
+
] }),
|
|
3077
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("section", { style: cardStyle2, children: [
|
|
3078
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { style: cardTitleStyle, children: "Starting from another example?" }),
|
|
3079
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { style: cardTextStyle, children: [
|
|
3080
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "npx create-grist-widget list" }),
|
|
3081
|
+
" shows the examples, and",
|
|
3082
|
+
" ",
|
|
3083
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { style: codeStyle, children: "npx create-grist-widget use <example>" }),
|
|
3084
|
+
" swaps this widget for one."
|
|
3085
|
+
] })
|
|
3086
|
+
] })
|
|
3087
|
+
] });
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
// src/app/chrome/release-page.tsx
|
|
3091
|
+
var import_react6 = require("react");
|
|
3092
|
+
|
|
3093
|
+
// src/app/deploy-path.ts
|
|
3094
|
+
var VERSION_SEGMENT = /^v(\d+\.\d+\.\d+(?:-[\w.]+)?)$/;
|
|
3095
|
+
function parseDeployPath(pathname) {
|
|
3096
|
+
const segments = pathname.replace(/\/+$/, "").split("/");
|
|
3097
|
+
const last = segments[segments.length - 1] ?? "";
|
|
3098
|
+
let channel = null;
|
|
3099
|
+
if (last === "latest") channel = { kind: "latest" };
|
|
3100
|
+
else if (last === "dev") channel = { kind: "dev" };
|
|
3101
|
+
else {
|
|
3102
|
+
const match = VERSION_SEGMENT.exec(last);
|
|
3103
|
+
if (match) channel = { kind: "version", version: match[1] };
|
|
3104
|
+
}
|
|
3105
|
+
if (!channel) return { channel: { kind: "latest" }, basePath: withTrailingSlash(pathname) };
|
|
3106
|
+
return { channel, basePath: withTrailingSlash(segments.slice(0, -1).join("/")) };
|
|
3107
|
+
}
|
|
3108
|
+
function deployChannelLabel(channel) {
|
|
3109
|
+
return channel.kind === "version" ? `v${channel.version}` : channel.kind;
|
|
3110
|
+
}
|
|
3111
|
+
function withTrailingSlash(path) {
|
|
3112
|
+
return path.endsWith("/") ? path : `${path}/`;
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
// src/app/released-versions.ts
|
|
3116
|
+
var import_react5 = require("react");
|
|
3117
|
+
function useReleasedVersions(pathname = typeof window === "undefined" ? "/" : window.location.pathname) {
|
|
3118
|
+
const url = `${parseDeployPath(pathname).basePath}versions.json`;
|
|
3119
|
+
const [result, setResult] = (0, import_react5.useState)({ versions: null, error: false });
|
|
3120
|
+
(0, import_react5.useEffect)(() => {
|
|
3121
|
+
let cancelled = false;
|
|
3122
|
+
fetch(url, { cache: "no-store" }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`HTTP ${res.status}`))).then((data) => {
|
|
3123
|
+
if (cancelled) return;
|
|
3124
|
+
if (!Array.isArray(data)) throw new Error("versions.json is not an array");
|
|
3125
|
+
setResult({ versions: data, error: false });
|
|
3126
|
+
}).catch(() => {
|
|
3127
|
+
if (!cancelled) setResult({ versions: null, error: true });
|
|
3128
|
+
});
|
|
3129
|
+
return () => {
|
|
3130
|
+
cancelled = true;
|
|
3131
|
+
};
|
|
3132
|
+
}, [url]);
|
|
3133
|
+
return result;
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
// src/app/chrome/release-page.tsx
|
|
3137
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
3138
|
+
function ReleasePage({ channel }) {
|
|
3139
|
+
const label = deployChannelLabel(channel);
|
|
3140
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("main", { style: pageStyle, children: [
|
|
3141
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: eyebrowStyle, children: [
|
|
3142
|
+
widgetTitle(),
|
|
3143
|
+
" \xB7 ",
|
|
3144
|
+
label
|
|
3145
|
+
] }),
|
|
3146
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h1", { style: headingStyle, children: "Grist isn't loaded here" }),
|
|
3147
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: leadStyle, children: [
|
|
3148
|
+
"This is the ",
|
|
3149
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { style: codeStyle, children: label }),
|
|
3150
|
+
" build, opened outside Grist. Hover a version to see when it was released."
|
|
3151
|
+
] }),
|
|
3152
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLinks, { current: channel }),
|
|
3153
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(InstallUrl, {})
|
|
3154
|
+
] });
|
|
3155
|
+
}
|
|
3156
|
+
function ChannelLinks({ current }) {
|
|
3157
|
+
const { basePath } = parseDeployPath(window.location.pathname);
|
|
3158
|
+
const { versions } = useReleasedVersions();
|
|
3159
|
+
const isCurrent = (channel) => channel.kind === current.kind && (channel.kind !== "version" || current.kind === "version" && channel.version === current.version);
|
|
3160
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("nav", { "aria-label": "Versions", style: { display: "flex", flexWrap: "wrap", justifyContent: "center", gap: "0.5rem", marginBottom: "2rem" }, children: [
|
|
3161
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLink, { href: `${basePath}latest/`, active: isCurrent({ kind: "latest" }), children: "latest" }),
|
|
3162
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ChannelLink, { href: `${basePath}dev/`, active: isCurrent({ kind: "dev" }), children: "dev" }),
|
|
3163
|
+
versions?.map((v) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
3164
|
+
ChannelLink,
|
|
3165
|
+
{
|
|
3166
|
+
href: `${basePath}v${v.version}/`,
|
|
3167
|
+
title: formatDate(v.publishedAt),
|
|
3168
|
+
active: isCurrent({ kind: "version", version: v.version }),
|
|
3169
|
+
children: `v${v.version}`
|
|
3170
|
+
},
|
|
3171
|
+
v.version
|
|
3172
|
+
))
|
|
3173
|
+
] });
|
|
3174
|
+
}
|
|
3175
|
+
function ChannelLink({
|
|
3176
|
+
href,
|
|
3177
|
+
title,
|
|
3178
|
+
active,
|
|
3179
|
+
children
|
|
3180
|
+
}) {
|
|
3181
|
+
const style = {
|
|
3182
|
+
padding: "0.25rem 0.75rem",
|
|
3183
|
+
border: `1px solid ${active ? tokens.primary : tokens.border}`,
|
|
3184
|
+
borderRadius: "9999px",
|
|
3185
|
+
background: active ? tokens.primary : "transparent",
|
|
3186
|
+
color: active ? tokens.primaryForeground : tokens.mutedForeground,
|
|
3187
|
+
fontSize: "0.75rem",
|
|
3188
|
+
fontWeight: 500,
|
|
3189
|
+
textDecoration: "none"
|
|
3190
|
+
};
|
|
3191
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { href, title, "aria-current": active ? "page" : void 0, style, children });
|
|
3192
|
+
}
|
|
3193
|
+
function InstallUrl() {
|
|
3194
|
+
const url = window.location.href;
|
|
3195
|
+
const [copied, setCopied] = (0, import_react6.useState)(false);
|
|
3196
|
+
function copy() {
|
|
3197
|
+
void navigator.clipboard?.writeText(url).then(() => {
|
|
3198
|
+
setCopied(true);
|
|
3199
|
+
setTimeout(() => setCopied(false), 1500);
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("section", { style: cardStyle2, children: [
|
|
3203
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h2", { style: cardTitleStyle, children: "Use it as a Grist custom widget" }),
|
|
3204
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { ...cardTextStyle, marginBottom: "1rem" }, children: "Paste this URL into a custom widget's URL field in Grist." }),
|
|
3205
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: "0.5rem" }, children: [
|
|
3206
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
3207
|
+
"input",
|
|
3208
|
+
{
|
|
3209
|
+
readOnly: true,
|
|
3210
|
+
value: url,
|
|
3211
|
+
"aria-label": "Widget URL",
|
|
3212
|
+
onFocus: (event) => event.target.select(),
|
|
3213
|
+
style: {
|
|
3214
|
+
flex: 1,
|
|
3215
|
+
minWidth: 0,
|
|
3216
|
+
padding: "0.5rem 0.75rem",
|
|
3217
|
+
border: `1px solid ${tokens.border}`,
|
|
3218
|
+
borderRadius: tokens.radius,
|
|
3219
|
+
background: "transparent",
|
|
3220
|
+
color: tokens.foreground,
|
|
3221
|
+
font: "inherit",
|
|
3222
|
+
fontSize: "0.875rem"
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
),
|
|
3226
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
3227
|
+
"button",
|
|
3228
|
+
{
|
|
3229
|
+
type: "button",
|
|
3230
|
+
onClick: copy,
|
|
3231
|
+
style: {
|
|
3232
|
+
padding: "0.5rem 1rem",
|
|
3233
|
+
border: "none",
|
|
3234
|
+
borderRadius: tokens.radius,
|
|
3235
|
+
background: tokens.primary,
|
|
3236
|
+
color: tokens.primaryForeground,
|
|
3237
|
+
font: "inherit",
|
|
3238
|
+
fontSize: "0.875rem",
|
|
3239
|
+
fontWeight: 500,
|
|
3240
|
+
cursor: "pointer"
|
|
3241
|
+
},
|
|
3242
|
+
children: copied ? "Copied" : "Copy"
|
|
3243
|
+
}
|
|
3244
|
+
)
|
|
3245
|
+
] })
|
|
3246
|
+
] });
|
|
3247
|
+
}
|
|
3248
|
+
function formatDate(iso) {
|
|
3249
|
+
const date = new Date(iso);
|
|
3250
|
+
return Number.isNaN(date.getTime()) ? iso : date.toLocaleDateString(void 0, { year: "numeric", month: "short", day: "numeric" });
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// src/sdk/helpers/grist-sdk-alerts.ts
|
|
3254
|
+
var TITLE = {
|
|
3255
|
+
"column-mapping": "Column mapping",
|
|
3256
|
+
"mapping-pending": "Column mapping",
|
|
3257
|
+
"section-not-linked": "Widget linking",
|
|
3258
|
+
"source-not-wired": "Widget linking",
|
|
3259
|
+
"action-error": "Action error",
|
|
3260
|
+
"access-insufficient": "Access level",
|
|
3261
|
+
"map-back-skip": "Write skipped",
|
|
3262
|
+
"cell-issues": "Unexpected values"
|
|
3263
|
+
};
|
|
3264
|
+
var SEVERITY = {
|
|
3265
|
+
"column-mapping": "warning",
|
|
3266
|
+
"mapping-pending": "info",
|
|
3267
|
+
"section-not-linked": "warning",
|
|
3268
|
+
"source-not-wired": "warning",
|
|
3269
|
+
"action-error": "error",
|
|
3270
|
+
"access-insufficient": "error",
|
|
3271
|
+
"map-back-skip": "warning",
|
|
3272
|
+
"cell-issues": "warning"
|
|
3273
|
+
};
|
|
3274
|
+
var alert = (kind, message2, ariaRole = "status") => ({
|
|
3275
|
+
id: kind,
|
|
3276
|
+
kind,
|
|
3277
|
+
severity: SEVERITY[kind],
|
|
3278
|
+
title: TITLE[kind],
|
|
3279
|
+
ariaRole,
|
|
3280
|
+
message: message2
|
|
3281
|
+
});
|
|
3282
|
+
function isWidgetSectionNotLinked(interaction) {
|
|
3283
|
+
const linking = parseGristWidgetLinkingInfo(interaction?.linking);
|
|
3284
|
+
return Boolean(linking) && linking.asTarget == null;
|
|
3285
|
+
}
|
|
3286
|
+
function getGristSdkAlerts(state, widget, options = {}) {
|
|
3287
|
+
if (deriveStatus(state) !== "ready") return [];
|
|
3288
|
+
const out = [];
|
|
3289
|
+
const mapping = deriveColumnMappingStatus(widget.columns, state.mappings);
|
|
3290
|
+
if (mapping.pending) out.push(alert("mapping-pending", "Waiting for Grist to report column mappings."));
|
|
3291
|
+
else if (!mapping.ok) {
|
|
3292
|
+
const parts = ["Column mapping is incomplete."];
|
|
3293
|
+
if (mapping.missing.length) parts.push(`Required columns: ${mapping.missing.join(", ")}.`);
|
|
3294
|
+
if (mapping.emptyMultiples.length) parts.push(`Assign at least one sheet column for: ${mapping.emptyMultiples.join(", ")}.`);
|
|
3295
|
+
out.push(alert("column-mapping", parts.join(" ")));
|
|
3296
|
+
}
|
|
3297
|
+
const failure = state.actionError ?? state.readError;
|
|
3298
|
+
if (failure && isAccessInsufficientError(failure)) {
|
|
3299
|
+
out.push(
|
|
3300
|
+
alert(
|
|
3301
|
+
"access-insufficient",
|
|
3302
|
+
'This operation requires "Full document access". Set the Access level to "Full document access" in the Custom widget configuration panel.',
|
|
3303
|
+
"alert"
|
|
3304
|
+
)
|
|
3305
|
+
);
|
|
3306
|
+
} else if (state.actionError) {
|
|
3307
|
+
out.push(alert("action-error", state.actionError, "alert"));
|
|
3308
|
+
}
|
|
3309
|
+
if (state.mapBackSkipped.length) {
|
|
3310
|
+
const list = state.mapBackSkipped.join(", ");
|
|
3311
|
+
out.push(alert("map-back-skip", `mapBack skipped multi-mapped column${state.mapBackSkipped.length === 1 ? "" : "s"}: ${list}.`));
|
|
3312
|
+
}
|
|
3313
|
+
const cellIssues = widget.cellIssues ?? [];
|
|
3314
|
+
if (cellIssues.length) {
|
|
3315
|
+
const first = cellIssues[0];
|
|
3316
|
+
const where = first.column ? `${first.column} in row ${first.rowId}` : `row ${first.rowId}`;
|
|
3317
|
+
const more = cellIssues.length > 1 ? ` (and ${cellIssues.length - 1} more)` : "";
|
|
3318
|
+
out.push(alert("cell-issues", `${where}: ${first.message}${more}. Check the column's type, or the cell, in Grist.`));
|
|
3319
|
+
}
|
|
3320
|
+
const linking = parseGristWidgetLinkingInfo(state.interaction?.linking);
|
|
3321
|
+
if (widget.allowSelectBy) {
|
|
3322
|
+
if (linking && !linking.asSource) {
|
|
3323
|
+
out.push(
|
|
3324
|
+
alert(
|
|
3325
|
+
"source-not-wired",
|
|
3326
|
+
"This widget is configured as a selector (allowSelectBy) but no other section is using it as a link source yet. Link another section to read from this widget."
|
|
3327
|
+
)
|
|
3328
|
+
);
|
|
3329
|
+
}
|
|
3330
|
+
} else if (isWidgetSectionNotLinked(state.interaction)) {
|
|
3331
|
+
const hint = options.sectionNotLinkedHint?.trim();
|
|
3332
|
+
out.push(
|
|
3333
|
+
alert(
|
|
3334
|
+
"section-not-linked",
|
|
3335
|
+
"This widget section is not linked to a table or selector. Link it from the page settings so the selected row is sent to the widget." + (hint ? ` ${hint}` : "")
|
|
3336
|
+
)
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
const suppressed = widget.suppressAlerts ?? [];
|
|
3340
|
+
return out.filter((a) => !suppressed.includes(a.kind));
|
|
3341
|
+
}
|
|
3342
|
+
function useGristSdkAlerts(options) {
|
|
3343
|
+
const { connection, state, cellIssues } = useNearestGristState();
|
|
3344
|
+
if (!connection) return [];
|
|
3345
|
+
const { columns, allowSelectBy, suppressAlerts } = connection.runtime.options;
|
|
3346
|
+
return getGristSdkAlerts(state, { columns, allowSelectBy, suppressAlerts, cellIssues }, options);
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
// src/app/chrome/widget-alerts.tsx
|
|
3350
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
3351
|
+
var ALERT_OPTIONS = {
|
|
3352
|
+
sectionNotLinkedHint: "In Grist, select this widget section on the page, then use Link to connect a table, card, or chart view as the selector."
|
|
3353
|
+
};
|
|
3354
|
+
function WidgetAlerts({ children }) {
|
|
3355
|
+
const alerts = useGristSdkAlerts(ALERT_OPTIONS);
|
|
3356
|
+
if (alerts.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children });
|
|
3357
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "0.75rem" }, children: [
|
|
3358
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: "0.75rem", padding: "0.75rem 0.75rem 0" }, children: alerts.map((alert2) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WidgetAlert, { alert: alert2 }, alert2.id)) }),
|
|
3359
|
+
children
|
|
3360
|
+
] });
|
|
3361
|
+
}
|
|
3362
|
+
function WidgetAlert({ alert: alert2 }) {
|
|
3363
|
+
const linkIcon = alert2.kind === "section-not-linked" || alert2.kind === "source-not-wired";
|
|
3364
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { role: alert2.ariaRole, style: { ...alertStyle, ...SEVERITY2[alert2.severity] }, children: [
|
|
3365
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { "aria-hidden": true, style: { flexShrink: 0, marginTop: "0.125rem" }, children: linkIcon ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(LinkIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WarningIcon, {}) }),
|
|
3366
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { minWidth: 0 }, children: [
|
|
3367
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { margin: 0, fontWeight: 500 }, children: alert2.title }),
|
|
3368
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { margin: "0.25rem 0 0", color: tokens.mutedForeground }, children: alert2.message })
|
|
3369
|
+
] })
|
|
3370
|
+
] });
|
|
3371
|
+
}
|
|
3372
|
+
var alertStyle = {
|
|
3373
|
+
boxSizing: "border-box",
|
|
3374
|
+
display: "flex",
|
|
3375
|
+
gap: "0.75rem",
|
|
3376
|
+
padding: "0.75rem 1rem",
|
|
3377
|
+
border: `1px solid ${tokens.border}`,
|
|
3378
|
+
borderRadius: tokens.radius,
|
|
3379
|
+
color: tokens.foreground,
|
|
3380
|
+
fontSize: "0.875rem",
|
|
3381
|
+
lineHeight: 1.5
|
|
3382
|
+
};
|
|
3383
|
+
var SEVERITY2 = {
|
|
3384
|
+
error: { borderColor: tokens.destructive, color: tokens.destructive },
|
|
3385
|
+
warning: { borderColor: "rgb(245 158 11 / 0.4)", background: "rgb(245 158 11 / 0.1)" },
|
|
3386
|
+
info: { background: "transparent" }
|
|
3387
|
+
};
|
|
3388
|
+
var ICON = {
|
|
3389
|
+
width: 16,
|
|
3390
|
+
height: 16,
|
|
3391
|
+
viewBox: "0 0 24 24",
|
|
3392
|
+
fill: "none",
|
|
3393
|
+
stroke: "currentColor",
|
|
3394
|
+
strokeWidth: 2,
|
|
3395
|
+
strokeLinecap: "round",
|
|
3396
|
+
strokeLinejoin: "round"
|
|
3397
|
+
};
|
|
3398
|
+
function WarningIcon() {
|
|
3399
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { ...ICON, children: [
|
|
3400
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }),
|
|
3401
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 9v4" }),
|
|
3402
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 17h.01" })
|
|
3403
|
+
] });
|
|
3404
|
+
}
|
|
3405
|
+
function LinkIcon() {
|
|
3406
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("svg", { ...ICON, children: [
|
|
3407
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M9 17H7A5 5 0 0 1 7 7h2" }),
|
|
3408
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M15 7h2a5 5 0 1 1 0 10h-2" }),
|
|
3409
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "8", x2: "16", y1: "12", y2: "12" })
|
|
3410
|
+
] });
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
// src/app/widget-theme.tsx
|
|
3414
|
+
var import_react7 = require("react");
|
|
3415
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
3416
|
+
var STORAGE_KEY = "theme";
|
|
3417
|
+
var DARK_QUERY = "(prefers-color-scheme: dark)";
|
|
3418
|
+
var THEMES = ["light", "dark", "system"];
|
|
3419
|
+
var ThemeContext = (0, import_react7.createContext)(null);
|
|
3420
|
+
function EmbeddedThemeProvider({ children }) {
|
|
3421
|
+
const gristTheme = useNearestGristState().state.theme;
|
|
3422
|
+
const system = useSystemTheme(gristTheme === null);
|
|
3423
|
+
const resolvedTheme = gristTheme ?? system;
|
|
3424
|
+
useApplyTheme(resolvedTheme);
|
|
3425
|
+
const value = (0, import_react7.useMemo)(
|
|
3426
|
+
() => ({ theme: gristTheme ?? "system", resolvedTheme, setTheme: noop }),
|
|
3427
|
+
[gristTheme, resolvedTheme]
|
|
3428
|
+
);
|
|
3429
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ThemeContext.Provider, { value, children });
|
|
3430
|
+
}
|
|
3431
|
+
function StandaloneThemeProvider({ children }) {
|
|
3432
|
+
const [theme, setThemeState] = (0, import_react7.useState)(() => readStoredTheme() ?? "system");
|
|
3433
|
+
const system = useSystemTheme(theme === "system");
|
|
3434
|
+
const resolvedTheme = theme === "system" ? system : theme;
|
|
3435
|
+
useApplyTheme(resolvedTheme);
|
|
3436
|
+
const setTheme = (0, import_react7.useCallback)((next) => {
|
|
3437
|
+
writeStoredTheme(next);
|
|
3438
|
+
setThemeState(next);
|
|
3439
|
+
}, []);
|
|
3440
|
+
(0, import_react7.useEffect)(() => {
|
|
3441
|
+
const onKeyDown = (event) => {
|
|
3442
|
+
if (event.repeat || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
3443
|
+
if (event.key.toLowerCase() !== "d" || isEditableTarget(event.target)) return;
|
|
3444
|
+
setThemeState((current) => {
|
|
3445
|
+
const currentResolved = current === "system" ? systemTheme() : current;
|
|
3446
|
+
const next = currentResolved === "dark" ? "light" : "dark";
|
|
3447
|
+
writeStoredTheme(next);
|
|
3448
|
+
return next;
|
|
3449
|
+
});
|
|
3450
|
+
};
|
|
3451
|
+
window.addEventListener("keydown", onKeyDown);
|
|
3452
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
3453
|
+
}, []);
|
|
3454
|
+
(0, import_react7.useEffect)(() => {
|
|
3455
|
+
const onStorage = (event) => {
|
|
3456
|
+
if (event.key !== STORAGE_KEY) return;
|
|
3457
|
+
setThemeState(isTheme(event.newValue) ? event.newValue : "system");
|
|
3458
|
+
};
|
|
3459
|
+
window.addEventListener("storage", onStorage);
|
|
3460
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
3461
|
+
}, []);
|
|
3462
|
+
const value = (0, import_react7.useMemo)(
|
|
3463
|
+
() => ({ theme, resolvedTheme, setTheme }),
|
|
3464
|
+
[theme, resolvedTheme, setTheme]
|
|
3465
|
+
);
|
|
3466
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ThemeContext.Provider, { value, children });
|
|
3467
|
+
}
|
|
3468
|
+
function useSystemTheme(active) {
|
|
3469
|
+
const [theme, setTheme] = (0, import_react7.useState)(systemTheme);
|
|
3470
|
+
(0, import_react7.useEffect)(() => {
|
|
3471
|
+
if (!active || typeof window.matchMedia !== "function") return;
|
|
3472
|
+
const query = window.matchMedia(DARK_QUERY);
|
|
3473
|
+
const onChange = () => setTheme(query.matches ? "dark" : "light");
|
|
3474
|
+
onChange();
|
|
3475
|
+
query.addEventListener("change", onChange);
|
|
3476
|
+
return () => query.removeEventListener("change", onChange);
|
|
3477
|
+
}, [active]);
|
|
3478
|
+
return theme;
|
|
3479
|
+
}
|
|
3480
|
+
function useApplyTheme(theme) {
|
|
3481
|
+
(0, import_react7.useEffect)(() => {
|
|
3482
|
+
const root = document.documentElement;
|
|
3483
|
+
if (root.classList.contains(theme) && !root.classList.contains(other(theme))) return;
|
|
3484
|
+
const restoreTransitions = suspendTransitions();
|
|
3485
|
+
root.classList.remove(other(theme));
|
|
3486
|
+
root.classList.add(theme);
|
|
3487
|
+
restoreTransitions();
|
|
3488
|
+
}, [theme]);
|
|
3489
|
+
}
|
|
3490
|
+
function systemTheme() {
|
|
3491
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return "light";
|
|
3492
|
+
return window.matchMedia(DARK_QUERY).matches ? "dark" : "light";
|
|
3493
|
+
}
|
|
3494
|
+
function other(theme) {
|
|
3495
|
+
return theme === "dark" ? "light" : "dark";
|
|
3496
|
+
}
|
|
3497
|
+
function suspendTransitions() {
|
|
3498
|
+
const style = document.createElement("style");
|
|
3499
|
+
style.appendChild(
|
|
3500
|
+
document.createTextNode("*,*::before,*::after{-webkit-transition:none!important;transition:none!important}")
|
|
3501
|
+
);
|
|
3502
|
+
document.head.appendChild(style);
|
|
3503
|
+
return () => {
|
|
3504
|
+
window.getComputedStyle(document.body);
|
|
3505
|
+
requestAnimationFrame(() => requestAnimationFrame(() => style.remove()));
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
function isEditableTarget(target) {
|
|
3509
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
3510
|
+
return target.isContentEditable || target.closest("input, textarea, select, [contenteditable='true']") !== null;
|
|
3511
|
+
}
|
|
3512
|
+
function isTheme(value) {
|
|
3513
|
+
return value !== null && THEMES.includes(value);
|
|
3514
|
+
}
|
|
3515
|
+
function readStoredTheme() {
|
|
3516
|
+
try {
|
|
3517
|
+
const value = localStorage.getItem(STORAGE_KEY);
|
|
3518
|
+
return isTheme(value) ? value : null;
|
|
3519
|
+
} catch {
|
|
3520
|
+
return null;
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
function writeStoredTheme(theme) {
|
|
3524
|
+
try {
|
|
3525
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
3526
|
+
} catch {
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
function noop() {
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
// src/app/mount-grist-widget.tsx
|
|
3533
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
3534
|
+
function GristWidgetApp({
|
|
3535
|
+
App,
|
|
3536
|
+
options,
|
|
3537
|
+
dev = false,
|
|
3538
|
+
Alerts = WidgetAlerts,
|
|
3539
|
+
LocalDevPage: LocalDevPage2 = LocalDevPage,
|
|
3540
|
+
ReleasePage: ReleasePage2 = ReleasePage
|
|
3541
|
+
}) {
|
|
3542
|
+
const Wrap = Alerts ?? PassThrough;
|
|
3543
|
+
if (isEmbedded2()) {
|
|
3544
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(GristWidgetProvider, { options, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(EmbeddedThemeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(GristBoundary, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: FILL, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Wrap, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react8.Suspense, { fallback: null, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(App, {}) }) }) }) }) }) });
|
|
3545
|
+
}
|
|
3546
|
+
const channel = parseDeployPath(window.location.pathname).channel;
|
|
3547
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StandaloneThemeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: FILL, children: dev ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(LocalDevPage2, {}) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ReleasePage2, { channel }) }) });
|
|
3548
|
+
}
|
|
3549
|
+
var FILL = { minHeight: "100%", width: "100%" };
|
|
3550
|
+
function isEmbedded2() {
|
|
3551
|
+
return typeof window !== "undefined" && window.self !== window.top;
|
|
3552
|
+
}
|
|
3553
|
+
function PassThrough({ children }) {
|
|
3554
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children });
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
// src/emulator/testing/jsdom-stubs.ts
|
|
3558
|
+
function installJsdomStubs() {
|
|
3559
|
+
if (typeof window === "undefined") return;
|
|
3560
|
+
globalThis.ResizeObserver ?? (globalThis.ResizeObserver = class ResizeObserver {
|
|
3561
|
+
observe() {
|
|
3562
|
+
}
|
|
3563
|
+
unobserve() {
|
|
3564
|
+
}
|
|
3565
|
+
disconnect() {
|
|
3566
|
+
}
|
|
3567
|
+
});
|
|
3568
|
+
window.matchMedia ?? (window.matchMedia = (query) => ({
|
|
3569
|
+
matches: false,
|
|
3570
|
+
media: query,
|
|
3571
|
+
onchange: null,
|
|
3572
|
+
addEventListener() {
|
|
3573
|
+
},
|
|
3574
|
+
removeEventListener() {
|
|
3575
|
+
},
|
|
3576
|
+
addListener() {
|
|
3577
|
+
},
|
|
3578
|
+
removeListener() {
|
|
3579
|
+
},
|
|
3580
|
+
dispatchEvent: () => false
|
|
3581
|
+
}));
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// src/emulator/testing/smoke-test-widget.tsx
|
|
3585
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
3586
|
+
async function smokeTestWidget({
|
|
3587
|
+
App,
|
|
3588
|
+
options,
|
|
3589
|
+
document: document2,
|
|
3590
|
+
timeout = 1e4
|
|
3591
|
+
}) {
|
|
3592
|
+
installJsdomStubs();
|
|
3593
|
+
const crash = { error: void 0 };
|
|
3594
|
+
function Probe() {
|
|
3595
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CatchRenderError, { crash, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { "data-grist-smoke-test": "", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(App, {}) }) });
|
|
3596
|
+
}
|
|
3597
|
+
const columns = declaredColumns(options);
|
|
3598
|
+
const gristDocument = document2 ?? smokeTestDocument(columns);
|
|
3599
|
+
const result = renderWithGrist(/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GristWidgetApp, { App: Probe, options }), {
|
|
3600
|
+
emulator: {
|
|
3601
|
+
document: gristDocument,
|
|
3602
|
+
// Granted as the user would in Grist's widget panel: the access the
|
|
3603
|
+
// widget asks for, unless the given document says otherwise.
|
|
3604
|
+
widgetInteraction: gristDocument.widgetInteraction ?? { access_level: options.requiredAccess ?? "read table" }
|
|
3605
|
+
}
|
|
3606
|
+
});
|
|
3607
|
+
try {
|
|
3608
|
+
if (columns.length > 0) result.emulator.setColumnMappings(mapEveryColumn(columns));
|
|
3609
|
+
try {
|
|
3610
|
+
await (0, import_react10.waitFor)(
|
|
3611
|
+
() => {
|
|
3612
|
+
if (crash.error !== void 0) return;
|
|
3613
|
+
const probe = result.container.querySelector("[data-grist-smoke-test]");
|
|
3614
|
+
if (!probe || !probe.hasChildNodes()) throw new Error("not rendered yet");
|
|
3615
|
+
},
|
|
3616
|
+
{ timeout }
|
|
3617
|
+
);
|
|
3618
|
+
} catch {
|
|
3619
|
+
const shown = result.container.textContent?.trim().replace(/\s+/g, " ") || "nothing";
|
|
3620
|
+
throw new Error(
|
|
3621
|
+
`smokeTestWidget: the widget didn't render past GristBoundary within ${timeout} ms. The page showed: "${shown.slice(0, 300)}".`
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
if (crash.error !== void 0) throw crash.error;
|
|
3625
|
+
} finally {
|
|
3626
|
+
result.unmount();
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
var CatchRenderError = class extends import_react9.Component {
|
|
3630
|
+
constructor() {
|
|
3631
|
+
super(...arguments);
|
|
3632
|
+
this.state = { failed: false };
|
|
3633
|
+
}
|
|
3634
|
+
static getDerivedStateFromError() {
|
|
3635
|
+
return { failed: true };
|
|
3636
|
+
}
|
|
3637
|
+
componentDidCatch(error) {
|
|
3638
|
+
this.props.crash.error = error;
|
|
3639
|
+
}
|
|
3640
|
+
render() {
|
|
3641
|
+
return this.state.failed ? null : this.props.children;
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
function declaredColumns(options) {
|
|
3645
|
+
return (options.columns ?? []).map(
|
|
3646
|
+
(column) => typeof column === "string" ? { name: column, type: "Any", allowMultiple: false } : { name: column.name, type: firstType(column.type), allowMultiple: column.allowMultiple === true }
|
|
3647
|
+
);
|
|
3648
|
+
}
|
|
3649
|
+
function firstType(type) {
|
|
3650
|
+
return (type ?? "Any").split(",")[0].trim() || "Any";
|
|
3651
|
+
}
|
|
3652
|
+
function mapEveryColumn(columns) {
|
|
3653
|
+
return Object.fromEntries(columns.map((c) => [c.name, c.allowMultiple ? [c.name] : c.name]));
|
|
3654
|
+
}
|
|
3655
|
+
function smokeTestDocument(columns) {
|
|
3656
|
+
return {
|
|
3657
|
+
generatedAt: "1970-01-01T00:00:00.000Z",
|
|
3658
|
+
docName: "Smoke test",
|
|
3659
|
+
mode: "schema+data",
|
|
3660
|
+
tables: {
|
|
3661
|
+
Table1: {
|
|
3662
|
+
columns: Object.fromEntries(columns.map((c) => [c.name, { type: c.type }])),
|
|
3663
|
+
rows: [{ id: 1, ...Object.fromEntries(columns.map((c) => [c.name, sampleValue(c.type)])) }]
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
function sampleValue(type) {
|
|
3669
|
+
switch (type) {
|
|
3670
|
+
case "Numeric":
|
|
3671
|
+
case "Int":
|
|
3672
|
+
return 42;
|
|
3673
|
+
case "Bool":
|
|
3674
|
+
return true;
|
|
3675
|
+
case "Date":
|
|
3676
|
+
case "DateTime":
|
|
3677
|
+
return 1767225600;
|
|
3678
|
+
// 2026-01-01, as Grist stores it: epoch seconds
|
|
3679
|
+
default:
|
|
3680
|
+
return "Sample";
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
|
|
1993
3684
|
// src/emulator/testing/matchers.ts
|
|
1994
3685
|
function actionsOf(emulator) {
|
|
1995
3686
|
return emulator.bus.history().filter((e) => e.type === "user-action").map((e) => e.action);
|
|
@@ -2044,7 +3735,7 @@ function waitForEvent(emulator, kindOrPredicate, timeoutOrOptions = 1e3) {
|
|
|
2044
3735
|
}
|
|
2045
3736
|
|
|
2046
3737
|
// src/emulator/testing/index.ts
|
|
2047
|
-
var
|
|
3738
|
+
var import_react11 = require("@testing-library/react");
|
|
2048
3739
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2049
3740
|
0 && (module.exports = {
|
|
2050
3741
|
act,
|
|
@@ -2052,10 +3743,12 @@ var import_react2 = require("@testing-library/react");
|
|
|
2052
3743
|
cleanup,
|
|
2053
3744
|
eventsOf,
|
|
2054
3745
|
fireEvent,
|
|
3746
|
+
installJsdomStubs,
|
|
2055
3747
|
presets,
|
|
2056
3748
|
render,
|
|
2057
3749
|
renderWithGrist,
|
|
2058
3750
|
screen,
|
|
3751
|
+
smokeTestWidget,
|
|
2059
3752
|
waitFor,
|
|
2060
3753
|
waitForEvent,
|
|
2061
3754
|
within
|