grist-widget-sdk 0.2.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 +730 -0
- package/LICENSE +15 -0
- package/README.md +249 -0
- package/dist/advanced.cjs +2749 -0
- package/dist/advanced.cjs.map +1 -0
- package/dist/advanced.d.cts +790 -0
- package/dist/advanced.d.ts +790 -0
- package/dist/advanced.js +825 -0
- package/dist/advanced.js.map +1 -0
- package/dist/chunk-64SVRWW7.js +758 -0
- package/dist/chunk-64SVRWW7.js.map +1 -0
- package/dist/chunk-BNBV24WM.js +479 -0
- package/dist/chunk-BNBV24WM.js.map +1 -0
- package/dist/chunk-ELGVOHFD.js +32 -0
- package/dist/chunk-ELGVOHFD.js.map +1 -0
- package/dist/chunk-GTTD5HPO.js +36 -0
- package/dist/chunk-GTTD5HPO.js.map +1 -0
- package/dist/chunk-JASMM2YC.js +243 -0
- package/dist/chunk-JASMM2YC.js.map +1 -0
- package/dist/chunk-PM3B4WYV.js +12 -0
- package/dist/chunk-PM3B4WYV.js.map +1 -0
- package/dist/chunk-QKXKZ27D.js +1189 -0
- package/dist/chunk-QKXKZ27D.js.map +1 -0
- package/dist/chunk-QZG7SNUG.js +463 -0
- package/dist/chunk-QZG7SNUG.js.map +1 -0
- package/dist/chunk-REV5AUXD.js +25 -0
- package/dist/chunk-REV5AUXD.js.map +1 -0
- package/dist/chunk-XE2SKNLH.js +1641 -0
- package/dist/chunk-XE2SKNLH.js.map +1 -0
- package/dist/create-grist-emulator-C_0I4wax.d.ts +80 -0
- package/dist/create-grist-emulator-DBS6gmiI.d.cts +80 -0
- package/dist/emulator/devtools.cjs +1367 -0
- package/dist/emulator/devtools.cjs.map +1 -0
- package/dist/emulator/devtools.d.cts +46 -0
- package/dist/emulator/devtools.d.ts +46 -0
- package/dist/emulator/devtools.js +480 -0
- package/dist/emulator/devtools.js.map +1 -0
- package/dist/emulator/testing.cjs +2061 -0
- package/dist/emulator/testing.cjs.map +1 -0
- package/dist/emulator/testing.d.cts +50 -0
- package/dist/emulator/testing.d.ts +50 -0
- package/dist/emulator/testing.js +124 -0
- package/dist/emulator/testing.js.map +1 -0
- package/dist/emulator/transports.cjs +1132 -0
- package/dist/emulator/transports.cjs.map +1 -0
- package/dist/emulator/transports.d.cts +37 -0
- package/dist/emulator/transports.d.ts +37 -0
- package/dist/emulator/transports.js +12 -0
- package/dist/emulator/transports.js.map +1 -0
- package/dist/emulator.cjs +1796 -0
- package/dist/emulator.cjs.map +1 -0
- package/dist/emulator.d.cts +107 -0
- package/dist/emulator.d.ts +107 -0
- package/dist/emulator.js +26 -0
- package/dist/emulator.js.map +1 -0
- package/dist/index.cjs +5511 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +825 -0
- package/dist/index.d.ts +825 -0
- package/dist/index.js +2447 -0
- package/dist/index.js.map +1 -0
- package/dist/inline-CRwQQ-GW.d.cts +311 -0
- package/dist/inline-QitZwXKP.d.ts +311 -0
- package/dist/templates-BC7FoSdW.d.ts +24 -0
- package/dist/templates-DBq719O_.d.cts +24 -0
- package/dist/types-Fb2wplkv.d.cts +228 -0
- package/dist/types-Fb2wplkv.d.ts +228 -0
- package/dist/use-grist-widget-options-from-context-BFhQBGRY.d.ts +522 -0
- package/dist/use-grist-widget-options-from-context-qpFMdoBJ.d.cts +522 -0
- package/package.json +131 -0
|
@@ -0,0 +1,1796 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/emulator/index.ts
|
|
21
|
+
var emulator_exports = {};
|
|
22
|
+
__export(emulator_exports, {
|
|
23
|
+
EmulatorActionError: () => EmulatorActionError,
|
|
24
|
+
createGristEmulator: () => createGristEmulator,
|
|
25
|
+
enrichEmulatorColumnMetadata: () => enrichEmulatorColumnMetadata,
|
|
26
|
+
parseDocument: () => parseDocument,
|
|
27
|
+
presets: () => presets,
|
|
28
|
+
resolveMappings: () => resolveMappings,
|
|
29
|
+
safeParseDocument: () => safeParseDocument
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(emulator_exports);
|
|
32
|
+
|
|
33
|
+
// src/emulator/api/command-api.ts
|
|
34
|
+
function createCommandApiImpl(bus) {
|
|
35
|
+
return {
|
|
36
|
+
async run(name) {
|
|
37
|
+
bus.emit({ type: "user-action", action: ["RunCommand", name] });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/emulator/core/mappings.ts
|
|
43
|
+
function resolveMappings(state) {
|
|
44
|
+
if (state.selection.mappings !== void 0 && state.selection.mappings !== null) {
|
|
45
|
+
return state.selection.mappings;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/emulator/api/custom-section-api.ts
|
|
51
|
+
function createCustomSectionApiImpl(state, bus) {
|
|
52
|
+
return {
|
|
53
|
+
async configure(settings) {
|
|
54
|
+
const config = {
|
|
55
|
+
requiredAccess: settings.requiredAccess,
|
|
56
|
+
columns: settings.columns,
|
|
57
|
+
allowSelectBy: settings.allowSelectBy,
|
|
58
|
+
hasCustomOptions: settings.hasCustomOptions
|
|
59
|
+
};
|
|
60
|
+
state.configuration = config;
|
|
61
|
+
bus.emit({ type: "ready", configuration: config });
|
|
62
|
+
bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
|
|
63
|
+
},
|
|
64
|
+
async mappings() {
|
|
65
|
+
return resolveMappings(state);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/emulator/api/grist-api.ts
|
|
71
|
+
function createGristApiImpl() {
|
|
72
|
+
return {
|
|
73
|
+
async ping() {
|
|
74
|
+
return "pong";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/sdk/helpers/grist-value-codec.ts
|
|
80
|
+
function coerceGristDocApiRefRowId(value) {
|
|
81
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
82
|
+
if (!Array.isArray(value) || value.length < 2) return void 0;
|
|
83
|
+
const tag = value[0];
|
|
84
|
+
if (tag === "R" && typeof value[2] === "number" && Number.isFinite(value[2])) return value[2];
|
|
85
|
+
if (tag === "r" && typeof value[1] === "number" && Number.isFinite(value[1])) return value[1];
|
|
86
|
+
return void 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/sdk/helpers/grist-safe-parse.ts
|
|
90
|
+
var import_zod = require("zod");
|
|
91
|
+
var gristSafeRefValueSchema = import_zod.z.object({
|
|
92
|
+
rowId: import_zod.z.number(),
|
|
93
|
+
display: import_zod.z.string().nullable().optional()
|
|
94
|
+
});
|
|
95
|
+
var gristSafeRefListValueSchema = import_zod.z.object({
|
|
96
|
+
rowIds: import_zod.z.array(import_zod.z.number()),
|
|
97
|
+
displays: import_zod.z.array(import_zod.z.string()).optional()
|
|
98
|
+
});
|
|
99
|
+
var gristCellValueSchema = import_zod.z.union([
|
|
100
|
+
import_zod.z.null(),
|
|
101
|
+
import_zod.z.string(),
|
|
102
|
+
import_zod.z.number(),
|
|
103
|
+
import_zod.z.boolean(),
|
|
104
|
+
import_zod.z.date(),
|
|
105
|
+
import_zod.z.array(import_zod.z.unknown()),
|
|
106
|
+
gristSafeRefValueSchema,
|
|
107
|
+
gristSafeRefListValueSchema
|
|
108
|
+
]);
|
|
109
|
+
function inferDisplayHelpersByRef(columns) {
|
|
110
|
+
const out = {};
|
|
111
|
+
const helperColumns = Object.entries(columns).filter(
|
|
112
|
+
([colId]) => colId.startsWith("gristHelper_")
|
|
113
|
+
);
|
|
114
|
+
const usedHelpers = /* @__PURE__ */ new Set();
|
|
115
|
+
const colRefToColId = /* @__PURE__ */ new Map();
|
|
116
|
+
for (const [colId, col] of Object.entries(columns)) {
|
|
117
|
+
if (typeof col?.colRef === "number" && Number.isFinite(col.colRef)) {
|
|
118
|
+
colRefToColId.set(col.colRef, colId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const [colId, col] of Object.entries(columns)) {
|
|
122
|
+
const type = col?.type;
|
|
123
|
+
if (typeof type !== "string" || !type.startsWith("Ref:") && !type.startsWith("RefList:")) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (typeof col.displayColRef === "number" && Number.isFinite(col.displayColRef)) {
|
|
127
|
+
const helperColId = colRefToColId.get(col.displayColRef);
|
|
128
|
+
if (helperColId) {
|
|
129
|
+
out[colId] = helperColId;
|
|
130
|
+
usedHelpers.add(helperColId);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const match = helperColumns.find(([, helper]) => {
|
|
135
|
+
const formula = helper?.formula;
|
|
136
|
+
return typeof formula === "string" && (formula.includes(`$${colId}.`) || formula.includes(`$${colId}`));
|
|
137
|
+
});
|
|
138
|
+
if (match) {
|
|
139
|
+
out[colId] = match[0];
|
|
140
|
+
usedHelpers.add(match[0]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const unresolvedRefs = Object.entries(columns).filter(([colId, col]) => {
|
|
144
|
+
const type = col?.type;
|
|
145
|
+
return !out[colId] && typeof type === "string" && (type.startsWith("Ref:") || type.startsWith("RefList:"));
|
|
146
|
+
}).sort((a, b) => (a[1]?.parentPos ?? Number.MAX_SAFE_INTEGER) - (b[1]?.parentPos ?? Number.MAX_SAFE_INTEGER));
|
|
147
|
+
const helperByPos = helperColumns.filter(([helperId]) => !usedHelpers.has(helperId)).sort((a, b) => (a[1]?.parentPos ?? Number.MAX_SAFE_INTEGER) - (b[1]?.parentPos ?? Number.MAX_SAFE_INTEGER));
|
|
148
|
+
for (const [refId, refCol] of unresolvedRefs) {
|
|
149
|
+
const refPos = refCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
|
|
150
|
+
const candidate = helperByPos.find(([, helperCol]) => {
|
|
151
|
+
const helperPos = helperCol?.parentPos ?? Number.MAX_SAFE_INTEGER;
|
|
152
|
+
return helperPos > refPos;
|
|
153
|
+
});
|
|
154
|
+
if (!candidate) continue;
|
|
155
|
+
out[refId] = candidate[0];
|
|
156
|
+
usedHelpers.add(candidate[0]);
|
|
157
|
+
}
|
|
158
|
+
for (const [refId] of unresolvedRefs) {
|
|
159
|
+
if (out[refId]) continue;
|
|
160
|
+
const candidate = helperByPos.find(([helperId]) => !usedHelpers.has(helperId));
|
|
161
|
+
if (!candidate) continue;
|
|
162
|
+
out[refId] = candidate[0];
|
|
163
|
+
usedHelpers.add(candidate[0]);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/sdk/helpers/replica-normalize.ts
|
|
169
|
+
function normalizeReplicaTableInput(table) {
|
|
170
|
+
return { ...table, rows: table.rows ?? [] };
|
|
171
|
+
}
|
|
172
|
+
function normalizeReplicaDocumentInput(doc) {
|
|
173
|
+
return {
|
|
174
|
+
...doc,
|
|
175
|
+
tables: Object.fromEntries(
|
|
176
|
+
Object.entries(doc.tables).map(([tableId, t]) => [tableId, normalizeReplicaTableInput(t)])
|
|
177
|
+
)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/emulator/core/document.ts
|
|
182
|
+
var SYSTEM_COLUMN = "id";
|
|
183
|
+
var EMULATOR_JS_FORMULA_PLACEHOLDER = "(emulator JavaScript formula)";
|
|
184
|
+
function enrichEmulatorColumnMetadata(col) {
|
|
185
|
+
const originalTrimmed = col.formula?.trim() ?? "";
|
|
186
|
+
const explicitInferred = col.inferredFormula?.trim() ?? "";
|
|
187
|
+
if (col.formulaFn) {
|
|
188
|
+
const inferredFormula = explicitInferred || originalTrimmed || EMULATOR_JS_FORMULA_PLACEHOLDER;
|
|
189
|
+
return {
|
|
190
|
+
...col,
|
|
191
|
+
...originalTrimmed ? { formula: col.formula } : {},
|
|
192
|
+
inferredFormula
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (explicitInferred || originalTrimmed) {
|
|
196
|
+
return {
|
|
197
|
+
...col,
|
|
198
|
+
inferredFormula: explicitInferred || originalTrimmed || void 0
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return col;
|
|
202
|
+
}
|
|
203
|
+
function sortEmulatorColumnsByParentPos(columns) {
|
|
204
|
+
const entries = Object.entries(columns);
|
|
205
|
+
entries.sort(([ka, a], [kb, b]) => {
|
|
206
|
+
const pa = a.parentPos;
|
|
207
|
+
const pb = b.parentPos;
|
|
208
|
+
if (pa !== void 0 && pb !== void 0 && pa !== pb) return pa - pb;
|
|
209
|
+
if (pa !== void 0 && pb === void 0) return -1;
|
|
210
|
+
if (pa === void 0 && pb !== void 0) return 1;
|
|
211
|
+
return ka.localeCompare(kb);
|
|
212
|
+
});
|
|
213
|
+
return Object.fromEntries(entries);
|
|
214
|
+
}
|
|
215
|
+
function inferColumnsFromRows(rows) {
|
|
216
|
+
const columns = {};
|
|
217
|
+
for (const row of rows) {
|
|
218
|
+
for (const key of Object.keys(row)) {
|
|
219
|
+
if (key === SYSTEM_COLUMN) continue;
|
|
220
|
+
if (!columns[key]) columns[key] = inferColumnType(row[key]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return columns;
|
|
224
|
+
}
|
|
225
|
+
function inferColumnType(sample) {
|
|
226
|
+
switch (typeof sample) {
|
|
227
|
+
case "number":
|
|
228
|
+
return { type: "Numeric" };
|
|
229
|
+
case "boolean":
|
|
230
|
+
return { type: "Bool" };
|
|
231
|
+
case "string":
|
|
232
|
+
return { type: "Text" };
|
|
233
|
+
default:
|
|
234
|
+
return { type: "Any" };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function buildTable(tableId, input) {
|
|
238
|
+
const rows = input.rows ?? [];
|
|
239
|
+
const inferred = inferColumnsFromRows(rows);
|
|
240
|
+
const merged = {};
|
|
241
|
+
for (const [colId, col] of Object.entries({ ...inferred, ...input.columns ?? {} })) {
|
|
242
|
+
merged[colId] = enrichEmulatorColumnMetadata(col);
|
|
243
|
+
}
|
|
244
|
+
const columns = sortEmulatorColumnsByParentPos(merged);
|
|
245
|
+
return {
|
|
246
|
+
tableId,
|
|
247
|
+
label: input.label,
|
|
248
|
+
columns,
|
|
249
|
+
rows: rows.map((row) => ({ ...row }))
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function createDocument(input) {
|
|
253
|
+
if (!input.tables || Object.keys(input.tables).length === 0) {
|
|
254
|
+
throw new Error("createDocument: at least one table is required");
|
|
255
|
+
}
|
|
256
|
+
const normalized = normalizeReplicaDocumentInput(input);
|
|
257
|
+
const tables = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const [tableId, tableInput] of Object.entries(normalized.tables)) {
|
|
259
|
+
tables.set(tableId, buildTable(tableId, tableInput));
|
|
260
|
+
}
|
|
261
|
+
const firstTableId = tables.keys().next().value;
|
|
262
|
+
const requestedTableId = normalized.selection?.tableId;
|
|
263
|
+
const selectedTableId = requestedTableId && tables.has(requestedTableId) ? requestedTableId : firstTableId;
|
|
264
|
+
const selectedTable = tables.get(selectedTableId);
|
|
265
|
+
const fallbackRowId = typeof selectedTable.rows[0]?.id === "number" ? selectedTable.rows[0].id : null;
|
|
266
|
+
const selection = {
|
|
267
|
+
tableId: selectedTableId,
|
|
268
|
+
rowId: normalized.selection?.rowId ?? fallbackRowId,
|
|
269
|
+
mappings: normalized.selection?.mappings,
|
|
270
|
+
cursor: normalized.selection?.cursor
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
docName: normalized.docName ?? "Untitled emulator document",
|
|
274
|
+
tables,
|
|
275
|
+
selection,
|
|
276
|
+
widgetOptions: { ...normalized.widgetOptions ?? {} },
|
|
277
|
+
widgetInteraction: { ...normalized.widgetInteraction ?? {} },
|
|
278
|
+
configuration: normalized.configuration ?? null,
|
|
279
|
+
accessToken: normalized.accessToken ?? null
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function getTable(state, tableId) {
|
|
283
|
+
const table = state.tables.get(tableId);
|
|
284
|
+
if (!table) throw new Error(`Unknown table: ${tableId}`);
|
|
285
|
+
return table;
|
|
286
|
+
}
|
|
287
|
+
function getSelectedTable(state) {
|
|
288
|
+
return getTable(state, state.selection.tableId);
|
|
289
|
+
}
|
|
290
|
+
function nextRowId(table) {
|
|
291
|
+
let max = 0;
|
|
292
|
+
for (const row of table.rows) {
|
|
293
|
+
if (typeof row.id === "number" && row.id > max) max = row.id;
|
|
294
|
+
}
|
|
295
|
+
return max + 1;
|
|
296
|
+
}
|
|
297
|
+
function findRow(table, rowId) {
|
|
298
|
+
return table.rows.find((r) => r.id === rowId);
|
|
299
|
+
}
|
|
300
|
+
function findRowIndex(table, rowId) {
|
|
301
|
+
return table.rows.findIndex((r) => r.id === rowId);
|
|
302
|
+
}
|
|
303
|
+
function evaluateRowFormulas(state, table, row) {
|
|
304
|
+
for (const [colId, col] of Object.entries(table.columns)) {
|
|
305
|
+
if (col.formulaFn) {
|
|
306
|
+
try {
|
|
307
|
+
row[colId] = col.formulaFn(row, { table, document: state });
|
|
308
|
+
} catch (err) {
|
|
309
|
+
row[colId] = formulaError(err);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function escapeRegExp(s) {
|
|
315
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
316
|
+
}
|
|
317
|
+
function extractRefListIdsFromWire(raw) {
|
|
318
|
+
if (raw == null) return [];
|
|
319
|
+
if (!Array.isArray(raw)) return [];
|
|
320
|
+
const items = raw[0] === "L" ? raw.slice(1) : raw;
|
|
321
|
+
const out = [];
|
|
322
|
+
for (const item of items) {
|
|
323
|
+
if (typeof item === "number" && Number.isFinite(item) && item > 0) {
|
|
324
|
+
out.push(Math.trunc(item));
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (item && typeof item === "object" && "rowId" in item) {
|
|
328
|
+
const r = item.rowId;
|
|
329
|
+
if (typeof r === "number" && Number.isFinite(r) && r > 0) {
|
|
330
|
+
out.push(Math.trunc(r));
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const t = coerceGristDocApiRefRowId(item);
|
|
335
|
+
if (t !== void 0 && t > 0) out.push(Math.trunc(t));
|
|
336
|
+
}
|
|
337
|
+
return Array.from(new Set(out));
|
|
338
|
+
}
|
|
339
|
+
function fieldFromRefListHelperFormula(formula, refColId) {
|
|
340
|
+
if (!formula) return null;
|
|
341
|
+
const re = new RegExp(`\\$${escapeRegExp(refColId)}\\.([A-Za-z_]\\w*)`);
|
|
342
|
+
const m = formula.match(re);
|
|
343
|
+
return m?.[1] ?? null;
|
|
344
|
+
}
|
|
345
|
+
function firstTextLikeColumnId(columns) {
|
|
346
|
+
let best = null;
|
|
347
|
+
for (const [colId, col] of Object.entries(columns)) {
|
|
348
|
+
if (colId === "id" || colId.startsWith("gristHelper_")) continue;
|
|
349
|
+
const t = col.type;
|
|
350
|
+
if (t !== "Text" && t !== "Choice") continue;
|
|
351
|
+
const pos = col.parentPos ?? 99999;
|
|
352
|
+
if (!best || pos < best.pos) best = { id: colId, pos };
|
|
353
|
+
}
|
|
354
|
+
return best?.id ?? null;
|
|
355
|
+
}
|
|
356
|
+
function formatLabelFromCell(value) {
|
|
357
|
+
if (value == null) return "";
|
|
358
|
+
if (typeof value === "string") return value.trim();
|
|
359
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
360
|
+
if (value instanceof Date) return value.toISOString();
|
|
361
|
+
if (Array.isArray(value) && value[0] === "E") return "";
|
|
362
|
+
if (typeof value === "object" && value && "__ref" in value) {
|
|
363
|
+
const r = value.rowId;
|
|
364
|
+
return typeof r === "number" && Number.isFinite(r) ? String(r) : "";
|
|
365
|
+
}
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
function syncRefListDisplayHelpers(state, table, row) {
|
|
369
|
+
const displayHelpersByRef = inferDisplayHelpersByRef(table.columns);
|
|
370
|
+
for (const [colId, col] of Object.entries(table.columns)) {
|
|
371
|
+
const type = col.type;
|
|
372
|
+
if (typeof type !== "string" || !type.startsWith("RefList:")) continue;
|
|
373
|
+
const helperColId = displayHelpersByRef[colId];
|
|
374
|
+
if (!helperColId) continue;
|
|
375
|
+
const helperMeta = table.columns[helperColId];
|
|
376
|
+
if (!helperMeta || helperMeta.formulaFn) continue;
|
|
377
|
+
const targetTableId = type.slice("RefList:".length);
|
|
378
|
+
const targetTable = state.tables.get(targetTableId);
|
|
379
|
+
if (!targetTable) continue;
|
|
380
|
+
const ids = extractRefListIdsFromWire(row[colId]);
|
|
381
|
+
if (ids.length === 0) {
|
|
382
|
+
row[helperColId] = [];
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const fieldId = fieldFromRefListHelperFormula(helperMeta.formula, colId) ?? firstTextLikeColumnId(targetTable.columns);
|
|
386
|
+
const labels = ids.map((rid) => {
|
|
387
|
+
if (!fieldId) return String(rid);
|
|
388
|
+
const trow = findRow(targetTable, rid);
|
|
389
|
+
if (!trow) return String(rid);
|
|
390
|
+
const lbl = formatLabelFromCell(trow[fieldId]);
|
|
391
|
+
return lbl !== "" ? lbl : String(rid);
|
|
392
|
+
});
|
|
393
|
+
row[helperColId] = labels;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function formulaError(err) {
|
|
397
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
398
|
+
return ["E", "Error", message];
|
|
399
|
+
}
|
|
400
|
+
function synthesizeTablesMeta(state) {
|
|
401
|
+
const ids = [];
|
|
402
|
+
const tableIds = [];
|
|
403
|
+
const labels = [];
|
|
404
|
+
let nextId = 1;
|
|
405
|
+
for (const [tableId, table] of state.tables.entries()) {
|
|
406
|
+
ids.push(nextId);
|
|
407
|
+
tableIds.push(tableId);
|
|
408
|
+
labels.push(table.label ?? null);
|
|
409
|
+
nextId += 1;
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
id: ids,
|
|
413
|
+
tableId: tableIds,
|
|
414
|
+
// Optional human-readable label; real Grist keeps more fields, but these
|
|
415
|
+
// are sufficient for `buildReplicaDocumentFromDocApi` to resolve table ids.
|
|
416
|
+
label: labels
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function synthesizeTablesColumnMeta(state) {
|
|
420
|
+
const parentIds = [];
|
|
421
|
+
const colIds = [];
|
|
422
|
+
const columnIds = [];
|
|
423
|
+
const types = [];
|
|
424
|
+
const labels = [];
|
|
425
|
+
const parentPos = [];
|
|
426
|
+
const isFormula = [];
|
|
427
|
+
const formulas = [];
|
|
428
|
+
const descriptions = [];
|
|
429
|
+
const widgetOptions = [];
|
|
430
|
+
const displayCol = [];
|
|
431
|
+
const visibleCol = [];
|
|
432
|
+
const tableIdToInternalId = /* @__PURE__ */ new Map();
|
|
433
|
+
{
|
|
434
|
+
let nextId = 1;
|
|
435
|
+
for (const [tableId] of state.tables.entries()) {
|
|
436
|
+
tableIdToInternalId.set(tableId, nextId);
|
|
437
|
+
nextId += 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const [tableId, table] of state.tables.entries()) {
|
|
441
|
+
const parentInternalId = tableIdToInternalId.get(tableId);
|
|
442
|
+
if (parentInternalId === void 0) continue;
|
|
443
|
+
for (const [colId, col] of Object.entries(table.columns)) {
|
|
444
|
+
parentIds.push(parentInternalId);
|
|
445
|
+
colIds.push(colId);
|
|
446
|
+
const ref = typeof col.colRef === "number" ? col.colRef : columnIds.length + 1;
|
|
447
|
+
columnIds.push(ref);
|
|
448
|
+
types.push(col.type ?? null);
|
|
449
|
+
labels.push(col.label ?? null);
|
|
450
|
+
parentPos.push(col.parentPos ?? null);
|
|
451
|
+
isFormula.push(Boolean(col.isFormula));
|
|
452
|
+
const formula = typeof col.formula === "string" ? col.formula : null;
|
|
453
|
+
formulas.push(formula);
|
|
454
|
+
descriptions.push(col.description ?? null);
|
|
455
|
+
widgetOptions.push(col.widgetOptions ?? null);
|
|
456
|
+
displayCol.push(typeof col.displayColRef === "number" ? col.displayColRef : null);
|
|
457
|
+
visibleCol.push(typeof col.visibleColRef === "number" ? col.visibleColRef : null);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
parentId: parentIds,
|
|
462
|
+
colId: colIds,
|
|
463
|
+
id: columnIds,
|
|
464
|
+
type: types,
|
|
465
|
+
label: labels,
|
|
466
|
+
parentPos,
|
|
467
|
+
isFormula,
|
|
468
|
+
formula: formulas,
|
|
469
|
+
description: descriptions,
|
|
470
|
+
// Also expose legacy `descr` for robustness.
|
|
471
|
+
descr: descriptions,
|
|
472
|
+
widgetOptions,
|
|
473
|
+
displayCol,
|
|
474
|
+
visibleCol
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/emulator/core/action-engine.ts
|
|
479
|
+
var EmulatorActionError = class extends Error {
|
|
480
|
+
constructor(action, message) {
|
|
481
|
+
super(message);
|
|
482
|
+
this.action = action;
|
|
483
|
+
this.name = "EmulatorActionError";
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
function applyUserActions(state, bus, actions) {
|
|
487
|
+
const retValues = [];
|
|
488
|
+
const changed = /* @__PURE__ */ new Set();
|
|
489
|
+
for (const action of actions) {
|
|
490
|
+
bus.emit({ type: "user-action", action });
|
|
491
|
+
const ret = applySingle(state, action, changed);
|
|
492
|
+
retValues.push(ret);
|
|
493
|
+
}
|
|
494
|
+
for (const tableId of changed) {
|
|
495
|
+
bus.emit({ type: "data-change", tableId });
|
|
496
|
+
}
|
|
497
|
+
return { retValues, changedTableIds: [...changed] };
|
|
498
|
+
}
|
|
499
|
+
function applySingle(state, action, changed) {
|
|
500
|
+
const [name, ...args] = action;
|
|
501
|
+
switch (name) {
|
|
502
|
+
case "AddRecord":
|
|
503
|
+
return addRecord(state, args, changed);
|
|
504
|
+
case "BulkAddRecord":
|
|
505
|
+
return bulkAddRecord(state, args, changed);
|
|
506
|
+
case "UpdateRecord":
|
|
507
|
+
return updateRecord(state, args, changed);
|
|
508
|
+
case "BulkUpdateRecord":
|
|
509
|
+
return bulkUpdateRecord(state, args, changed);
|
|
510
|
+
case "RemoveRecord":
|
|
511
|
+
return removeRecord(state, args, changed);
|
|
512
|
+
case "BulkRemoveRecord":
|
|
513
|
+
return bulkRemoveRecord(state, args, changed);
|
|
514
|
+
case "AddColumn":
|
|
515
|
+
return addColumn(state, args);
|
|
516
|
+
case "AddVisibleColumn":
|
|
517
|
+
return addColumn(state, args);
|
|
518
|
+
case "RemoveColumn":
|
|
519
|
+
return removeColumn(state, args, changed);
|
|
520
|
+
case "RenameColumn":
|
|
521
|
+
return renameColumn(state, args, changed);
|
|
522
|
+
case "ModifyColumn":
|
|
523
|
+
return modifyColumn(state, args, changed);
|
|
524
|
+
case "AddTable":
|
|
525
|
+
return addTable(state, args);
|
|
526
|
+
case "RemoveTable":
|
|
527
|
+
return removeTable(state, args);
|
|
528
|
+
case "RenameTable":
|
|
529
|
+
return renameTable(state, args);
|
|
530
|
+
default:
|
|
531
|
+
throw new EmulatorActionError(action, `Unsupported user action: ${String(name)}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function addRecord(state, [tableId, rowId, values], changed) {
|
|
535
|
+
const table = getTable(state, requireString(tableId, "AddRecord.tableId"));
|
|
536
|
+
const id = typeof rowId === "number" ? rowId : nextRowId(table);
|
|
537
|
+
const normalizedValues = normalizeValuesForTable(
|
|
538
|
+
table,
|
|
539
|
+
values ?? {}
|
|
540
|
+
);
|
|
541
|
+
const row = { id, ...normalizedValues };
|
|
542
|
+
ensureColumns(table, normalizedValues);
|
|
543
|
+
evaluateRowFormulas(state, table, row);
|
|
544
|
+
syncRefListDisplayHelpers(state, table, row);
|
|
545
|
+
table.rows.push(row);
|
|
546
|
+
changed.add(table.tableId);
|
|
547
|
+
return id;
|
|
548
|
+
}
|
|
549
|
+
function bulkAddRecord(state, [tableId, rowIds, columnar], changed) {
|
|
550
|
+
const table = getTable(state, requireString(tableId, "BulkAddRecord.tableId"));
|
|
551
|
+
const cols = normalizeColumnarForTable(
|
|
552
|
+
table,
|
|
553
|
+
columnar ?? {}
|
|
554
|
+
);
|
|
555
|
+
const ids = Array.isArray(rowIds) ? rowIds : [];
|
|
556
|
+
const length = ids.length || lengthOf(cols);
|
|
557
|
+
const out = [];
|
|
558
|
+
ensureColumns(table, cols, true);
|
|
559
|
+
for (let i = 0; i < length; i++) {
|
|
560
|
+
const id = typeof ids[i] === "number" ? ids[i] : nextRowId(table);
|
|
561
|
+
const row = { id };
|
|
562
|
+
for (const colId of Object.keys(cols)) row[colId] = cols[colId][i];
|
|
563
|
+
evaluateRowFormulas(state, table, row);
|
|
564
|
+
syncRefListDisplayHelpers(state, table, row);
|
|
565
|
+
table.rows.push(row);
|
|
566
|
+
out.push(id);
|
|
567
|
+
}
|
|
568
|
+
changed.add(table.tableId);
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
function updateRecord(state, [tableId, rowId, values], changed) {
|
|
572
|
+
const table = getTable(state, requireString(tableId, "UpdateRecord.tableId"));
|
|
573
|
+
const id = requireNumber(rowId, "UpdateRecord.rowId");
|
|
574
|
+
const row = findRow(table, id);
|
|
575
|
+
if (!row) throw new EmulatorActionError(["UpdateRecord", tableId, rowId, values], `Row ${id} not found in ${table.tableId}`);
|
|
576
|
+
const normalizedValues = normalizeValuesForTable(
|
|
577
|
+
table,
|
|
578
|
+
values ?? {}
|
|
579
|
+
);
|
|
580
|
+
ensureColumns(table, normalizedValues);
|
|
581
|
+
Object.assign(row, normalizedValues);
|
|
582
|
+
evaluateRowFormulas(state, table, row);
|
|
583
|
+
syncRefListDisplayHelpers(state, table, row);
|
|
584
|
+
changed.add(table.tableId);
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
function bulkUpdateRecord(state, [tableId, rowIds, columnar], changed) {
|
|
588
|
+
const table = getTable(state, requireString(tableId, "BulkUpdateRecord.tableId"));
|
|
589
|
+
const ids = rowIds;
|
|
590
|
+
const cols = normalizeColumnarForTable(
|
|
591
|
+
table,
|
|
592
|
+
columnar ?? {}
|
|
593
|
+
);
|
|
594
|
+
ensureColumns(table, cols, true);
|
|
595
|
+
ids.forEach((id, idx) => {
|
|
596
|
+
const row = findRow(table, id);
|
|
597
|
+
if (!row) throw new EmulatorActionError(["BulkUpdateRecord", tableId, ids, cols], `Row ${id} not found in ${table.tableId}`);
|
|
598
|
+
for (const colId of Object.keys(cols)) row[colId] = cols[colId][idx];
|
|
599
|
+
evaluateRowFormulas(state, table, row);
|
|
600
|
+
syncRefListDisplayHelpers(state, table, row);
|
|
601
|
+
});
|
|
602
|
+
changed.add(table.tableId);
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
function removeRecord(state, [tableId, rowId], changed) {
|
|
606
|
+
const table = getTable(state, requireString(tableId, "RemoveRecord.tableId"));
|
|
607
|
+
const id = requireNumber(rowId, "RemoveRecord.rowId");
|
|
608
|
+
const idx = findRowIndex(table, id);
|
|
609
|
+
if (idx >= 0) table.rows.splice(idx, 1);
|
|
610
|
+
if (state.selection.tableId === table.tableId && state.selection.rowId === id) {
|
|
611
|
+
state.selection.rowId = table.rows[0]?.id ?? null;
|
|
612
|
+
}
|
|
613
|
+
changed.add(table.tableId);
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
function bulkRemoveRecord(state, [tableId, rowIds], changed) {
|
|
617
|
+
const table = getTable(state, requireString(tableId, "BulkRemoveRecord.tableId"));
|
|
618
|
+
const ids = new Set(rowIds);
|
|
619
|
+
table.rows = table.rows.filter((r) => !ids.has(r.id));
|
|
620
|
+
if (state.selection.tableId === table.tableId && typeof state.selection.rowId === "number" && ids.has(state.selection.rowId)) {
|
|
621
|
+
state.selection.rowId = table.rows[0]?.id ?? null;
|
|
622
|
+
}
|
|
623
|
+
changed.add(table.tableId);
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
function addColumn(state, [tableId, colId, info]) {
|
|
627
|
+
const table = getTable(state, requireString(tableId, "AddColumn.tableId"));
|
|
628
|
+
const col = requireString(colId, "AddColumn.colId");
|
|
629
|
+
if (table.columns[col]) throw new EmulatorActionError(["AddColumn", tableId, colId], `Column ${col} already exists`);
|
|
630
|
+
table.columns[col] = info ?? {};
|
|
631
|
+
for (const row of table.rows) {
|
|
632
|
+
if (!(col in row)) row[col] = null;
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
function removeColumn(state, [tableId, colId], changed) {
|
|
637
|
+
const table = getTable(state, requireString(tableId, "RemoveColumn.tableId"));
|
|
638
|
+
const col = requireString(colId, "RemoveColumn.colId");
|
|
639
|
+
delete table.columns[col];
|
|
640
|
+
for (const row of table.rows) delete row[col];
|
|
641
|
+
changed.add(table.tableId);
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
function renameColumn(state, [tableId, colId, newColId], changed) {
|
|
645
|
+
const table = getTable(state, requireString(tableId, "RenameColumn.tableId"));
|
|
646
|
+
const from = requireString(colId, "RenameColumn.colId");
|
|
647
|
+
const to = requireString(newColId, "RenameColumn.newColId");
|
|
648
|
+
if (!table.columns[from]) throw new EmulatorActionError(["RenameColumn", tableId, from, to], `Column ${from} not found`);
|
|
649
|
+
if (from === to) return null;
|
|
650
|
+
table.columns[to] = table.columns[from];
|
|
651
|
+
delete table.columns[from];
|
|
652
|
+
for (const row of table.rows) {
|
|
653
|
+
if (from in row) {
|
|
654
|
+
row[to] = row[from];
|
|
655
|
+
delete row[from];
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
changed.add(table.tableId);
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
function modifyColumn(state, [tableId, colId, info], changed) {
|
|
662
|
+
const table = getTable(state, requireString(tableId, "ModifyColumn.tableId"));
|
|
663
|
+
const col = requireString(colId, "ModifyColumn.colId");
|
|
664
|
+
if (!table.columns[col]) throw new EmulatorActionError(["ModifyColumn", tableId, colId], `Column ${col} not found`);
|
|
665
|
+
table.columns[col] = { ...table.columns[col], ...info };
|
|
666
|
+
changed.add(table.tableId);
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
function addTable(state, [tableId, columns]) {
|
|
670
|
+
const id = requireString(tableId, "AddTable.tableId");
|
|
671
|
+
if (state.tables.has(id)) throw new EmulatorActionError(["AddTable", tableId], `Table ${id} already exists`);
|
|
672
|
+
const cols = {};
|
|
673
|
+
for (const c of columns ?? []) {
|
|
674
|
+
cols[c.id] = { ...c };
|
|
675
|
+
}
|
|
676
|
+
state.tables.set(id, { tableId: id, columns: cols, rows: [] });
|
|
677
|
+
return id;
|
|
678
|
+
}
|
|
679
|
+
function removeTable(state, [tableId]) {
|
|
680
|
+
const id = requireString(tableId, "RemoveTable.tableId");
|
|
681
|
+
state.tables.delete(id);
|
|
682
|
+
if (state.selection.tableId === id) {
|
|
683
|
+
const fallback = state.tables.keys().next().value;
|
|
684
|
+
if (fallback) {
|
|
685
|
+
state.selection.tableId = fallback;
|
|
686
|
+
state.selection.rowId = null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
function renameTable(state, [from, to]) {
|
|
692
|
+
const fromId = requireString(from, "RenameTable.from");
|
|
693
|
+
const toId = requireString(to, "RenameTable.to");
|
|
694
|
+
const table = state.tables.get(fromId);
|
|
695
|
+
if (!table) throw new EmulatorActionError(["RenameTable", fromId], `Table ${fromId} not found`);
|
|
696
|
+
state.tables.delete(fromId);
|
|
697
|
+
table.tableId = toId;
|
|
698
|
+
state.tables.set(toId, table);
|
|
699
|
+
if (state.selection.tableId === fromId) state.selection.tableId = toId;
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
function ensureColumns(table, values, bulk = false) {
|
|
703
|
+
for (const col of Object.keys(values)) {
|
|
704
|
+
if (!table.columns[col]) {
|
|
705
|
+
const sample = bulk ? values[col]?.[0] : values[col];
|
|
706
|
+
table.columns[col] = inferType(sample);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function inferType(sample) {
|
|
711
|
+
switch (typeof sample) {
|
|
712
|
+
case "number":
|
|
713
|
+
return { type: "Numeric" };
|
|
714
|
+
case "boolean":
|
|
715
|
+
return { type: "Bool" };
|
|
716
|
+
case "string":
|
|
717
|
+
return { type: "Text" };
|
|
718
|
+
default:
|
|
719
|
+
return { type: "Any" };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function lengthOf(cols) {
|
|
723
|
+
for (const arr of Object.values(cols)) if (Array.isArray(arr)) return arr.length;
|
|
724
|
+
return 0;
|
|
725
|
+
}
|
|
726
|
+
function requireString(value, name) {
|
|
727
|
+
if (typeof value !== "string" || !value) throw new Error(`Expected non-empty string for ${name}`);
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
function requireNumber(value, name) {
|
|
731
|
+
if (typeof value !== "number" || Number.isNaN(value)) throw new Error(`Expected number for ${name}`);
|
|
732
|
+
return value;
|
|
733
|
+
}
|
|
734
|
+
function normalizeValuesForTable(table, values) {
|
|
735
|
+
const out = {};
|
|
736
|
+
for (const [colId, value] of Object.entries(values)) {
|
|
737
|
+
out[colId] = normalizeCellValueForType(value, table.columns[colId]?.type);
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
function normalizeColumnarForTable(table, columns) {
|
|
742
|
+
const out = {};
|
|
743
|
+
for (const [colId, values] of Object.entries(columns)) {
|
|
744
|
+
const type = table.columns[colId]?.type;
|
|
745
|
+
out[colId] = Array.isArray(values) ? values.map((value) => normalizeCellValueForType(value, type)) : [];
|
|
746
|
+
}
|
|
747
|
+
return out;
|
|
748
|
+
}
|
|
749
|
+
function normalizeCellValueForType(value, type) {
|
|
750
|
+
if (value === null || value === void 0 || typeof type !== "string") return value;
|
|
751
|
+
if (type.startsWith("Ref:")) return normalizeRefValue(value);
|
|
752
|
+
if (type.startsWith("RefList:")) return normalizeRefListValue(value);
|
|
753
|
+
return value;
|
|
754
|
+
}
|
|
755
|
+
function extractRefRowId(value) {
|
|
756
|
+
if (value === null || value === void 0) return null;
|
|
757
|
+
if (value && typeof value === "object" && "__ref" in value) {
|
|
758
|
+
const inner = value.rowId;
|
|
759
|
+
return extractRefRowId(inner);
|
|
760
|
+
}
|
|
761
|
+
if (value && typeof value === "object" && "rowId" in value) {
|
|
762
|
+
return extractRefRowId(value.rowId);
|
|
763
|
+
}
|
|
764
|
+
const fromTuple = coerceGristDocApiRefRowId(value);
|
|
765
|
+
if (fromTuple !== void 0 && fromTuple > 0) return Math.trunc(fromTuple);
|
|
766
|
+
return toPositiveInt(value);
|
|
767
|
+
}
|
|
768
|
+
function normalizeRefValue(value) {
|
|
769
|
+
return extractRefRowId(value);
|
|
770
|
+
}
|
|
771
|
+
function normalizeRefListValue(value) {
|
|
772
|
+
const toIdList = (items) => Array.from(
|
|
773
|
+
new Set(items.map((item) => extractRefRowId(item)).filter((item) => item !== null))
|
|
774
|
+
);
|
|
775
|
+
if (Array.isArray(value) && value[0] === "L") {
|
|
776
|
+
return ["L", ...toIdList(value.slice(1))];
|
|
777
|
+
}
|
|
778
|
+
if (Array.isArray(value)) {
|
|
779
|
+
return ["L", ...toIdList(value)];
|
|
780
|
+
}
|
|
781
|
+
if (typeof value === "string") {
|
|
782
|
+
const tokens = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
783
|
+
return ["L", ...toIdList(tokens)];
|
|
784
|
+
}
|
|
785
|
+
if (value && typeof value === "object" && Array.isArray(value.rowIds)) {
|
|
786
|
+
return ["L", ...toIdList(value.rowIds)];
|
|
787
|
+
}
|
|
788
|
+
return ["L"];
|
|
789
|
+
}
|
|
790
|
+
function toPositiveInt(value) {
|
|
791
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.trunc(value);
|
|
792
|
+
if (typeof value === "string") {
|
|
793
|
+
const n = Number.parseInt(value.trim(), 10);
|
|
794
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/emulator/core/columnar.ts
|
|
800
|
+
function rowsToColumnar(table, rows, options = {}) {
|
|
801
|
+
const cols = Object.keys(table.columns);
|
|
802
|
+
const out = { id: rows.map((r) => r.id) };
|
|
803
|
+
for (const col of cols) {
|
|
804
|
+
out[col] = rows.map((r) => encodeForWire(r[col], table.columns[col]?.type, options.keepEncoded));
|
|
805
|
+
}
|
|
806
|
+
return out;
|
|
807
|
+
}
|
|
808
|
+
function rowToRecord(table, row, options = {}) {
|
|
809
|
+
const out = { id: row.id };
|
|
810
|
+
for (const col of Object.keys(table.columns)) {
|
|
811
|
+
out[col] = encodeForWire(row[col], table.columns[col]?.type, options.keepEncoded);
|
|
812
|
+
}
|
|
813
|
+
return out;
|
|
814
|
+
}
|
|
815
|
+
function encodeForWire(value, type, keepEncoded) {
|
|
816
|
+
if (value instanceof Date) {
|
|
817
|
+
const epoch = Math.floor(value.getTime() / 1e3);
|
|
818
|
+
if (keepEncoded) {
|
|
819
|
+
return type && type.startsWith("DateTime") ? ["D", epoch, "UTC"] : ["d", epoch];
|
|
820
|
+
}
|
|
821
|
+
return epoch;
|
|
822
|
+
}
|
|
823
|
+
return value;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/emulator/api/grist-doc-api.ts
|
|
827
|
+
function createGristDocApiImpl(state, bus) {
|
|
828
|
+
return {
|
|
829
|
+
async getDocName() {
|
|
830
|
+
return state.docName;
|
|
831
|
+
},
|
|
832
|
+
async listTables() {
|
|
833
|
+
return [...state.tables.keys()];
|
|
834
|
+
},
|
|
835
|
+
async fetchTable(tableId) {
|
|
836
|
+
if (tableId === "_grist_Tables") {
|
|
837
|
+
return synthesizeTablesMeta(state);
|
|
838
|
+
}
|
|
839
|
+
if (tableId === "_grist_Tables_column") {
|
|
840
|
+
return synthesizeTablesColumnMeta(state);
|
|
841
|
+
}
|
|
842
|
+
const table = getTable(state, tableId);
|
|
843
|
+
return rowsToColumnar(table, table.rows);
|
|
844
|
+
},
|
|
845
|
+
async applyUserActions(actions) {
|
|
846
|
+
const result = applyUserActions(state, bus, actions);
|
|
847
|
+
return { retValues: result.retValues };
|
|
848
|
+
},
|
|
849
|
+
async getAccessToken() {
|
|
850
|
+
if (!state.accessToken) {
|
|
851
|
+
const err = new Error("getAccessToken not available (no token configured on emulator)");
|
|
852
|
+
throw err;
|
|
853
|
+
}
|
|
854
|
+
return { ...state.accessToken };
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/emulator/api/grist-view.ts
|
|
860
|
+
function createGristViewImpl(state, bus) {
|
|
861
|
+
return {
|
|
862
|
+
async fetchSelectedTable(options) {
|
|
863
|
+
const table = getSelectedTable(state);
|
|
864
|
+
return rowsToColumnar(table, table.rows, { keepEncoded: options?.keepEncoded });
|
|
865
|
+
},
|
|
866
|
+
async fetchSelectedRecord(rowId, options) {
|
|
867
|
+
const table = getSelectedTable(state);
|
|
868
|
+
const row = findRow(table, rowId);
|
|
869
|
+
if (!row) throw new Error(`No row ${rowId} in ${table.tableId}`);
|
|
870
|
+
return rowToRecord(table, row, { keepEncoded: options?.keepEncoded });
|
|
871
|
+
},
|
|
872
|
+
async allowSelectBy() {
|
|
873
|
+
return state.configuration?.allowSelectBy ?? false;
|
|
874
|
+
},
|
|
875
|
+
async setSelectedRows(rowIds) {
|
|
876
|
+
if (!rowIds || rowIds.length === 0) {
|
|
877
|
+
state.selection.rowId = null;
|
|
878
|
+
} else {
|
|
879
|
+
state.selection.rowId = rowIds[0];
|
|
880
|
+
}
|
|
881
|
+
bus.emit({
|
|
882
|
+
type: "cursor-change",
|
|
883
|
+
tableId: state.selection.tableId,
|
|
884
|
+
rowId: state.selection.rowId
|
|
885
|
+
});
|
|
886
|
+
},
|
|
887
|
+
async setCursorPos(cursor) {
|
|
888
|
+
const tableId = state.selection.tableId;
|
|
889
|
+
if (typeof cursor.rowId === "number") state.selection.rowId = cursor.rowId;
|
|
890
|
+
state.selection.cursor = { ...state.selection.cursor, ...cursor };
|
|
891
|
+
bus.emit({
|
|
892
|
+
type: "cursor-change",
|
|
893
|
+
tableId,
|
|
894
|
+
rowId: state.selection.rowId,
|
|
895
|
+
cursor: state.selection.cursor
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/emulator/api/widget-api.ts
|
|
902
|
+
function createWidgetApiImpl(state, bus) {
|
|
903
|
+
function emit() {
|
|
904
|
+
bus.emit({
|
|
905
|
+
type: "options-change",
|
|
906
|
+
options: { ...state.widgetOptions },
|
|
907
|
+
interaction: { ...state.widgetInteraction }
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
async getOption(key) {
|
|
912
|
+
return state.widgetOptions[key];
|
|
913
|
+
},
|
|
914
|
+
async setOption(key, value) {
|
|
915
|
+
state.widgetOptions[key] = value;
|
|
916
|
+
emit();
|
|
917
|
+
},
|
|
918
|
+
async getOptions() {
|
|
919
|
+
return Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
|
|
920
|
+
},
|
|
921
|
+
async setOptions(options) {
|
|
922
|
+
state.widgetOptions = { ...state.widgetOptions, ...options };
|
|
923
|
+
emit();
|
|
924
|
+
},
|
|
925
|
+
async clearOptions() {
|
|
926
|
+
state.widgetOptions = {};
|
|
927
|
+
emit();
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/emulator/api/index.ts
|
|
933
|
+
function createApiBundle(state, bus) {
|
|
934
|
+
return {
|
|
935
|
+
GristView: createGristViewImpl(state, bus),
|
|
936
|
+
GristDocAPI: createGristDocApiImpl(state, bus),
|
|
937
|
+
WidgetAPI: createWidgetApiImpl(state, bus),
|
|
938
|
+
CustomSectionAPI: createCustomSectionApiImpl(state, bus),
|
|
939
|
+
GristAPI: createGristApiImpl(),
|
|
940
|
+
CommandAPI: createCommandApiImpl(bus)
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/emulator/core/event-bus.ts
|
|
945
|
+
function createEventBus(options = {}) {
|
|
946
|
+
const limit = options.historyLimit ?? 200;
|
|
947
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
948
|
+
const history = [];
|
|
949
|
+
return {
|
|
950
|
+
emit(event) {
|
|
951
|
+
history.push(event);
|
|
952
|
+
if (history.length > limit) history.shift();
|
|
953
|
+
for (const fn of [...listeners]) {
|
|
954
|
+
try {
|
|
955
|
+
fn(event);
|
|
956
|
+
} catch (err) {
|
|
957
|
+
console.error("[grist-emulator] event listener threw:", err);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
},
|
|
961
|
+
on(listener) {
|
|
962
|
+
listeners.add(listener);
|
|
963
|
+
return () => {
|
|
964
|
+
listeners.delete(listener);
|
|
965
|
+
};
|
|
966
|
+
},
|
|
967
|
+
history() {
|
|
968
|
+
return history;
|
|
969
|
+
},
|
|
970
|
+
clear() {
|
|
971
|
+
history.length = 0;
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// src/emulator/theme-message.ts
|
|
977
|
+
function buildGristThemeHostMessage(appearance, options) {
|
|
978
|
+
return {
|
|
979
|
+
theme: {
|
|
980
|
+
appearance,
|
|
981
|
+
name: appearance,
|
|
982
|
+
colors: {}
|
|
983
|
+
},
|
|
984
|
+
...options?.fromReady ? { fromReady: true } : {}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/emulator/transports/iframe.ts
|
|
989
|
+
var import_grain_rpc = require("grain-rpc");
|
|
990
|
+
function createIframeTransport(options) {
|
|
991
|
+
const { iframe, state, bus, api, targetOrigin = "*" } = options;
|
|
992
|
+
const timers = /* @__PURE__ */ new Set();
|
|
993
|
+
const rpc = new import_grain_rpc.Rpc({
|
|
994
|
+
sendMessage: (msg) => {
|
|
995
|
+
const target = iframe.contentWindow;
|
|
996
|
+
if (!target) return;
|
|
997
|
+
target.postMessage(msg, targetOrigin);
|
|
998
|
+
},
|
|
999
|
+
logger: { info: () => {
|
|
1000
|
+
}, warn: () => {
|
|
1001
|
+
} }
|
|
1002
|
+
});
|
|
1003
|
+
rpc.queueOutgoingUntilReadyMessage();
|
|
1004
|
+
const receiveFromWidget = rpc.receiveMessage.bind(rpc);
|
|
1005
|
+
rpc.receiveMessage = (msg) => {
|
|
1006
|
+
if (msg && typeof msg === "object" && msg.mtype === import_grain_rpc.MsgType.RpcCall && msg.mdest === "grist") {
|
|
1007
|
+
const copy = { ...msg };
|
|
1008
|
+
delete copy.mdest;
|
|
1009
|
+
receiveFromWidget(copy);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
receiveFromWidget(msg);
|
|
1013
|
+
};
|
|
1014
|
+
const onMessage = (event) => {
|
|
1015
|
+
if (event.source !== iframe.contentWindow) return;
|
|
1016
|
+
if (!isRpcEnvelope(event.data)) return;
|
|
1017
|
+
rpc.receiveMessage(event.data);
|
|
1018
|
+
};
|
|
1019
|
+
window.addEventListener("message", onMessage);
|
|
1020
|
+
rpc.registerImpl("GristView", api.GristView);
|
|
1021
|
+
rpc.registerImpl("GristDocAPI", api.GristDocAPI);
|
|
1022
|
+
rpc.registerImpl("WidgetAPI", api.WidgetAPI);
|
|
1023
|
+
rpc.registerImpl("CustomSectionAPI", api.CustomSectionAPI);
|
|
1024
|
+
rpc.registerImpl("_grist_api", api.GristAPI);
|
|
1025
|
+
rpc.registerImpl("CommandAPI", api.CommandAPI);
|
|
1026
|
+
const buildInteraction = options.buildInteraction ?? defaultInteraction(state);
|
|
1027
|
+
const cursorOf = options.cursor ?? (() => state.selection.cursor);
|
|
1028
|
+
const initialCursorOf = options.initialCursor ?? (() => state.selection.rowId);
|
|
1029
|
+
function fireDataChange(tableId, opts) {
|
|
1030
|
+
safePostMessage({
|
|
1031
|
+
tableId: tableId ?? state.selection.tableId,
|
|
1032
|
+
dataChange: true,
|
|
1033
|
+
...opts?.mappingsChange ? { mappingsChange: true } : {}
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
function fireCursor(rowId, opts) {
|
|
1037
|
+
const id = rowId === void 0 ? initialCursorOf() : rowId;
|
|
1038
|
+
if (id === null) return;
|
|
1039
|
+
safePostMessage({
|
|
1040
|
+
tableId: state.selection.tableId,
|
|
1041
|
+
rowId: id,
|
|
1042
|
+
cursor: cursorOf(),
|
|
1043
|
+
...opts?.mappingsChange ? { mappingsChange: true } : {}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
function fireOptions() {
|
|
1047
|
+
safePostMessage({
|
|
1048
|
+
options: hasOptions(state.widgetOptions) ? { ...state.widgetOptions } : null,
|
|
1049
|
+
settings: { ...buildInteraction() }
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function fireTheme(message) {
|
|
1053
|
+
safePostMessage(message);
|
|
1054
|
+
}
|
|
1055
|
+
function emitInitialState(opts) {
|
|
1056
|
+
fireOptions();
|
|
1057
|
+
if (options.initialTheme) {
|
|
1058
|
+
fireTheme(buildGristThemeHostMessage(options.initialTheme, { fromReady: true }));
|
|
1059
|
+
}
|
|
1060
|
+
fireDataChange(void 0, opts);
|
|
1061
|
+
fireCursor(void 0, opts);
|
|
1062
|
+
}
|
|
1063
|
+
function emitInitialStateWithReplay(opts) {
|
|
1064
|
+
emitInitialState(opts);
|
|
1065
|
+
queueMicrotask(() => emitInitialState(opts));
|
|
1066
|
+
const timer = setTimeout(() => {
|
|
1067
|
+
timers.delete(timer);
|
|
1068
|
+
emitInitialState(opts);
|
|
1069
|
+
}, 0);
|
|
1070
|
+
timers.add(timer);
|
|
1071
|
+
}
|
|
1072
|
+
const offBus = bus.on((event) => {
|
|
1073
|
+
switch (event.type) {
|
|
1074
|
+
case "ready":
|
|
1075
|
+
emitInitialStateWithReplay();
|
|
1076
|
+
break;
|
|
1077
|
+
case "data-change":
|
|
1078
|
+
fireDataChange(event.tableId);
|
|
1079
|
+
break;
|
|
1080
|
+
case "cursor-change": {
|
|
1081
|
+
const rowId = event.rowId === null ? state.selection.rowId : event.rowId;
|
|
1082
|
+
if (rowId !== null) {
|
|
1083
|
+
safePostMessage({
|
|
1084
|
+
tableId: event.tableId,
|
|
1085
|
+
rowId,
|
|
1086
|
+
cursor: cursorOf()
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
fireDataChange(event.tableId);
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
case "options-change":
|
|
1093
|
+
fireOptions();
|
|
1094
|
+
break;
|
|
1095
|
+
case "mappings-change":
|
|
1096
|
+
emitInitialStateWithReplay({ mappingsChange: true });
|
|
1097
|
+
break;
|
|
1098
|
+
case "theme-change":
|
|
1099
|
+
fireTheme(event.message);
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
rpc.processIncoming();
|
|
1104
|
+
function safePostMessage(payload) {
|
|
1105
|
+
try {
|
|
1106
|
+
rpc.postMessage(payload);
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
console.warn("[grist-emulator/iframe] failed to deliver postMessage", err);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
dispose() {
|
|
1113
|
+
window.removeEventListener("message", onMessage);
|
|
1114
|
+
offBus();
|
|
1115
|
+
for (const timer of timers) clearTimeout(timer);
|
|
1116
|
+
timers.clear();
|
|
1117
|
+
},
|
|
1118
|
+
fireDataChange,
|
|
1119
|
+
fireCursor,
|
|
1120
|
+
fireOptions,
|
|
1121
|
+
fireTheme,
|
|
1122
|
+
emitInitialState
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
function hasOptions(options) {
|
|
1126
|
+
return Object.keys(options).length > 0;
|
|
1127
|
+
}
|
|
1128
|
+
function isRpcEnvelope(data) {
|
|
1129
|
+
if (!data || typeof data !== "object") return false;
|
|
1130
|
+
return typeof data.mtype === "number";
|
|
1131
|
+
}
|
|
1132
|
+
function defaultInteraction(state) {
|
|
1133
|
+
return () => ({
|
|
1134
|
+
access_level: state.configuration?.requiredAccess ?? "read table",
|
|
1135
|
+
...state.widgetInteraction
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/sdk/helpers/grist-user-actions.ts
|
|
1140
|
+
function gristUpdateRecordAction(tableId, rowId, patch) {
|
|
1141
|
+
return ["UpdateRecord", tableId, rowId, patch];
|
|
1142
|
+
}
|
|
1143
|
+
function gristAddRecordAction(tableId, values) {
|
|
1144
|
+
return ["AddRecord", tableId, null, values];
|
|
1145
|
+
}
|
|
1146
|
+
function gristRemoveRecordAction(tableId, rowId) {
|
|
1147
|
+
return ["RemoveRecord", tableId, rowId];
|
|
1148
|
+
}
|
|
1149
|
+
function gristBulkUpdateRecordAction(tableId, rowIds, columnarPatch) {
|
|
1150
|
+
return ["BulkUpdateRecord", tableId, rowIds, columnarPatch];
|
|
1151
|
+
}
|
|
1152
|
+
function gristBulkAddRecordAction(tableId, values, rowIds) {
|
|
1153
|
+
const length = inferBulkLength(values);
|
|
1154
|
+
const ids = rowIds ?? new Array(length).fill(null);
|
|
1155
|
+
return ["BulkAddRecord", tableId, ids, values];
|
|
1156
|
+
}
|
|
1157
|
+
function gristBulkRemoveRecordAction(tableId, rowIds) {
|
|
1158
|
+
return ["BulkRemoveRecord", tableId, rowIds];
|
|
1159
|
+
}
|
|
1160
|
+
function inferBulkLength(values) {
|
|
1161
|
+
for (const arr of Object.values(values)) if (Array.isArray(arr)) return arr.length;
|
|
1162
|
+
return 0;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/emulator/transports/inline.ts
|
|
1166
|
+
function createInlineTransport(options) {
|
|
1167
|
+
const { state, bus, api } = options;
|
|
1168
|
+
const target = options.target ?? globalThis;
|
|
1169
|
+
installFakeIframeContext(target);
|
|
1170
|
+
const recordCbs = /* @__PURE__ */ new Map();
|
|
1171
|
+
const recordsCbs = /* @__PURE__ */ new Map();
|
|
1172
|
+
const optionsCbs = /* @__PURE__ */ new Set();
|
|
1173
|
+
const newRecordCbs = /* @__PURE__ */ new Set();
|
|
1174
|
+
const generic = /* @__PURE__ */ new Map();
|
|
1175
|
+
let lastRecord = null;
|
|
1176
|
+
let lastRecords = null;
|
|
1177
|
+
let lastOptions = null;
|
|
1178
|
+
let lastNewRecord = void 0;
|
|
1179
|
+
const buildInteraction = options.buildInteraction ?? (() => ({
|
|
1180
|
+
access_level: state.configuration?.requiredAccess ?? "read table",
|
|
1181
|
+
...state.widgetInteraction
|
|
1182
|
+
}));
|
|
1183
|
+
async function pushRecord(rowId) {
|
|
1184
|
+
const mappings = resolveMappings(state);
|
|
1185
|
+
if (rowId === "new") {
|
|
1186
|
+
lastNewRecord = mappings;
|
|
1187
|
+
for (const cb of newRecordCbs) {
|
|
1188
|
+
try {
|
|
1189
|
+
cb(mappings);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
logErr(err);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
lastRecord = { record: null, mappings };
|
|
1195
|
+
for (const [cb] of recordCbs) {
|
|
1196
|
+
try {
|
|
1197
|
+
cb(null, mappings);
|
|
1198
|
+
} catch (err) {
|
|
1199
|
+
logErr(err);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (rowId === null) {
|
|
1205
|
+
lastRecord = { record: null, mappings };
|
|
1206
|
+
for (const [cb] of recordCbs) {
|
|
1207
|
+
try {
|
|
1208
|
+
cb(null, mappings);
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
logErr(err);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
let record = null;
|
|
1216
|
+
try {
|
|
1217
|
+
record = await api.GristView.fetchSelectedRecord(rowId);
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
logErr(err);
|
|
1220
|
+
}
|
|
1221
|
+
lastRecord = { record, mappings };
|
|
1222
|
+
for (const [cb, opts] of recordCbs) {
|
|
1223
|
+
try {
|
|
1224
|
+
const r = opts?.keepEncoded ? await api.GristView.fetchSelectedRecord(rowId, { keepEncoded: true }) : record;
|
|
1225
|
+
cb(r, mappings);
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
logErr(err);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
async function pushRecords() {
|
|
1232
|
+
const mappings = resolveMappings(state);
|
|
1233
|
+
let baseRows = [];
|
|
1234
|
+
try {
|
|
1235
|
+
const columnar = await api.GristView.fetchSelectedTable({});
|
|
1236
|
+
baseRows = columnsToRows(columnar);
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
logErr(err);
|
|
1239
|
+
}
|
|
1240
|
+
lastRecords = { records: baseRows, mappings };
|
|
1241
|
+
for (const [cb, opts] of recordsCbs) {
|
|
1242
|
+
try {
|
|
1243
|
+
const fetchOptions = coerceFetchOptions(opts);
|
|
1244
|
+
const rows = fetchOptions?.keepEncoded ? columnsToRows(await api.GristView.fetchSelectedTable({ keepEncoded: true })) : baseRows;
|
|
1245
|
+
cb(rows, mappings);
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
logErr(err);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async function fetchSelectedTableShaped(opts) {
|
|
1252
|
+
const fetchOptions = coerceFetchOptions(opts);
|
|
1253
|
+
const columnar = await api.GristView.fetchSelectedTable(
|
|
1254
|
+
fetchOptions ? { keepEncoded: fetchOptions.keepEncoded } : {}
|
|
1255
|
+
);
|
|
1256
|
+
if (fetchOptions?.format === "rows") return columnsToRows(columnar);
|
|
1257
|
+
return columnar;
|
|
1258
|
+
}
|
|
1259
|
+
function pushOptions() {
|
|
1260
|
+
const opts = Object.keys(state.widgetOptions).length > 0 ? { ...state.widgetOptions } : null;
|
|
1261
|
+
const interaction = buildInteraction();
|
|
1262
|
+
lastOptions = { options: opts, interaction };
|
|
1263
|
+
for (const cb of optionsCbs) {
|
|
1264
|
+
try {
|
|
1265
|
+
cb(opts, interaction);
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
logErr(err);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const offBus = bus.on(async (event) => {
|
|
1272
|
+
switch (event.type) {
|
|
1273
|
+
case "data-change":
|
|
1274
|
+
await pushRecords();
|
|
1275
|
+
await pushRecord(state.selection.rowId);
|
|
1276
|
+
break;
|
|
1277
|
+
case "cursor-change":
|
|
1278
|
+
await pushRecord(event.rowId);
|
|
1279
|
+
break;
|
|
1280
|
+
case "options-change":
|
|
1281
|
+
pushOptions();
|
|
1282
|
+
break;
|
|
1283
|
+
case "ready":
|
|
1284
|
+
pushOptions();
|
|
1285
|
+
if (options.initialTheme) {
|
|
1286
|
+
notify("message", buildGristThemeHostMessage(options.initialTheme, { fromReady: true }));
|
|
1287
|
+
}
|
|
1288
|
+
await pushRecords();
|
|
1289
|
+
await pushRecord(state.selection.rowId);
|
|
1290
|
+
break;
|
|
1291
|
+
case "mappings-change":
|
|
1292
|
+
await pushRecords();
|
|
1293
|
+
await pushRecord(state.selection.rowId);
|
|
1294
|
+
break;
|
|
1295
|
+
case "theme-change":
|
|
1296
|
+
notify("message", event.message);
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
function makeTableOps(tableId) {
|
|
1301
|
+
return {
|
|
1302
|
+
async getTableId() {
|
|
1303
|
+
return tableId;
|
|
1304
|
+
},
|
|
1305
|
+
async create(records, options2) {
|
|
1306
|
+
const list = Array.isArray(records) ? records : [records];
|
|
1307
|
+
const ids = [];
|
|
1308
|
+
for (const r of list) {
|
|
1309
|
+
const result = applyUserActions(state, bus, [
|
|
1310
|
+
gristAddRecordAction(tableId, parseFields(r.fields, options2))
|
|
1311
|
+
]);
|
|
1312
|
+
ids.push({ id: result.retValues[0] });
|
|
1313
|
+
}
|
|
1314
|
+
return Array.isArray(records) ? ids : ids[0];
|
|
1315
|
+
},
|
|
1316
|
+
async update(records, options2) {
|
|
1317
|
+
const list = Array.isArray(records) ? records : [records];
|
|
1318
|
+
const actions = list.map((r) => gristUpdateRecordAction(tableId, r.id, parseFields(r.fields, options2)));
|
|
1319
|
+
applyUserActions(state, bus, actions);
|
|
1320
|
+
},
|
|
1321
|
+
async upsert(records, options2) {
|
|
1322
|
+
const list = Array.isArray(records) ? records : [records];
|
|
1323
|
+
const allowAdd = options2?.add !== false;
|
|
1324
|
+
const allowUpdate = options2?.update !== false;
|
|
1325
|
+
const table = getTable(state, tableId);
|
|
1326
|
+
const actions = [];
|
|
1327
|
+
for (const r of list) {
|
|
1328
|
+
const matches = table.rows.filter(
|
|
1329
|
+
(row) => Object.entries(r.require).every(([k, v]) => row[k] === v)
|
|
1330
|
+
);
|
|
1331
|
+
if (matches.length > 0) {
|
|
1332
|
+
if (!allowUpdate) continue;
|
|
1333
|
+
const fields = parseFields(r.fields ?? {}, options2);
|
|
1334
|
+
for (const m of matches) actions.push(gristUpdateRecordAction(tableId, m.id, fields));
|
|
1335
|
+
} else if (allowAdd) {
|
|
1336
|
+
actions.push(gristAddRecordAction(tableId, { ...r.require, ...parseFields(r.fields ?? {}, options2) }));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (actions.length > 0) applyUserActions(state, bus, actions);
|
|
1340
|
+
},
|
|
1341
|
+
async destroy(recordIds) {
|
|
1342
|
+
if (Array.isArray(recordIds)) {
|
|
1343
|
+
applyUserActions(state, bus, [gristBulkRemoveRecordAction(tableId, recordIds)]);
|
|
1344
|
+
} else {
|
|
1345
|
+
applyUserActions(state, bus, [gristRemoveRecordAction(tableId, recordIds)]);
|
|
1346
|
+
}
|
|
1347
|
+
void gristBulkAddRecordAction;
|
|
1348
|
+
void gristBulkUpdateRecordAction;
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
function notify(event, ...args) {
|
|
1353
|
+
const set = generic.get(event);
|
|
1354
|
+
if (!set) return;
|
|
1355
|
+
for (const fn of set) {
|
|
1356
|
+
try {
|
|
1357
|
+
fn(...args);
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
logErr(err);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const grist = {
|
|
1364
|
+
async ready(opts) {
|
|
1365
|
+
await api.CustomSectionAPI.configure(opts ? { ...opts, onEditOptions: void 0 } : {});
|
|
1366
|
+
},
|
|
1367
|
+
onRecord(cb, opts) {
|
|
1368
|
+
recordCbs.set(cb, opts);
|
|
1369
|
+
if (lastRecord) {
|
|
1370
|
+
try {
|
|
1371
|
+
cb(lastRecord.record, lastRecord.mappings);
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
logErr(err);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
},
|
|
1377
|
+
onRecords(cb, opts) {
|
|
1378
|
+
recordsCbs.set(cb, opts);
|
|
1379
|
+
if (lastRecords) {
|
|
1380
|
+
try {
|
|
1381
|
+
cb(lastRecords.records, lastRecords.mappings);
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
logErr(err);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
onOptions(cb) {
|
|
1388
|
+
optionsCbs.add(cb);
|
|
1389
|
+
if (lastOptions) {
|
|
1390
|
+
try {
|
|
1391
|
+
cb(lastOptions.options, lastOptions.interaction);
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
logErr(err);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
},
|
|
1397
|
+
onNewRecord(cb) {
|
|
1398
|
+
newRecordCbs.add(cb);
|
|
1399
|
+
if (lastNewRecord !== void 0) {
|
|
1400
|
+
try {
|
|
1401
|
+
cb(lastNewRecord);
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
logErr(err);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
1407
|
+
mapColumnNames(data) {
|
|
1408
|
+
const mappings = resolveMappings(state);
|
|
1409
|
+
if (!mappings) return null;
|
|
1410
|
+
const out = {};
|
|
1411
|
+
let allMapped = true;
|
|
1412
|
+
for (const [logical, real] of Object.entries(mappings)) {
|
|
1413
|
+
if (Array.isArray(real)) {
|
|
1414
|
+
out[logical] = real.map((r) => data[r]);
|
|
1415
|
+
} else if (typeof real === "string") {
|
|
1416
|
+
out[logical] = data[real];
|
|
1417
|
+
} else {
|
|
1418
|
+
allMapped = false;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return allMapped ? out : null;
|
|
1422
|
+
},
|
|
1423
|
+
mapColumnNamesBack(data) {
|
|
1424
|
+
const mappings = resolveMappings(state);
|
|
1425
|
+
if (!mappings) return data;
|
|
1426
|
+
const out = {};
|
|
1427
|
+
for (const [logical, real] of Object.entries(mappings)) {
|
|
1428
|
+
if (typeof real === "string" && logical in data) {
|
|
1429
|
+
out[real] = data[logical];
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return out;
|
|
1433
|
+
},
|
|
1434
|
+
getOption: (k) => api.WidgetAPI.getOption(k),
|
|
1435
|
+
setOption: (k, v) => api.WidgetAPI.setOption(k, v),
|
|
1436
|
+
getOptions: () => api.WidgetAPI.getOptions(),
|
|
1437
|
+
setOptions: (o) => api.WidgetAPI.setOptions(o),
|
|
1438
|
+
clearOptions: () => api.WidgetAPI.clearOptions(),
|
|
1439
|
+
setCursorPos: (pos) => api.GristView.setCursorPos(pos),
|
|
1440
|
+
setSelectedRows: (rowIds) => api.GristView.setSelectedRows(rowIds),
|
|
1441
|
+
fetchSelectedTable: (opts) => fetchSelectedTableShaped(opts),
|
|
1442
|
+
fetchSelectedRecord: (rowId, opts) => api.GristView.fetchSelectedRecord(rowId, coerceFetchOptions(opts)),
|
|
1443
|
+
docApi: {
|
|
1444
|
+
getDocName: () => api.GristDocAPI.getDocName(),
|
|
1445
|
+
listTables: () => api.GristDocAPI.listTables(),
|
|
1446
|
+
fetchTable: (id) => api.GristDocAPI.fetchTable(id),
|
|
1447
|
+
applyUserActions: (actions, options2) => api.GristDocAPI.applyUserActions(actions, options2),
|
|
1448
|
+
getAccessToken: (opts) => api.GristDocAPI.getAccessToken(opts),
|
|
1449
|
+
fetchSelectedTable: (opts) => fetchSelectedTableShaped(opts),
|
|
1450
|
+
fetchSelectedRecord: (rowId, opts) => api.GristView.fetchSelectedRecord(rowId, coerceFetchOptions(opts))
|
|
1451
|
+
},
|
|
1452
|
+
get selectedTable() {
|
|
1453
|
+
return makeTableOps(state.selection.tableId);
|
|
1454
|
+
},
|
|
1455
|
+
getTable(tableId) {
|
|
1456
|
+
return makeTableOps(tableId ?? state.selection.tableId);
|
|
1457
|
+
},
|
|
1458
|
+
sectionApi: {
|
|
1459
|
+
configure: (settings) => api.CustomSectionAPI.configure(settings),
|
|
1460
|
+
mappings: () => api.CustomSectionAPI.mappings()
|
|
1461
|
+
},
|
|
1462
|
+
on(eventName, listener) {
|
|
1463
|
+
let set = generic.get(eventName);
|
|
1464
|
+
if (!set) generic.set(eventName, set = /* @__PURE__ */ new Set());
|
|
1465
|
+
set.add(listener);
|
|
1466
|
+
return () => set.delete(listener);
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
target.grist = grist;
|
|
1470
|
+
return {
|
|
1471
|
+
grist,
|
|
1472
|
+
dispose() {
|
|
1473
|
+
offBus();
|
|
1474
|
+
const t = target;
|
|
1475
|
+
if (t.grist === grist) delete t.grist;
|
|
1476
|
+
recordCbs.clear();
|
|
1477
|
+
recordsCbs.clear();
|
|
1478
|
+
optionsCbs.clear();
|
|
1479
|
+
newRecordCbs.clear();
|
|
1480
|
+
generic.clear();
|
|
1481
|
+
lastRecord = null;
|
|
1482
|
+
lastRecords = null;
|
|
1483
|
+
lastOptions = null;
|
|
1484
|
+
lastNewRecord = void 0;
|
|
1485
|
+
},
|
|
1486
|
+
fireDataChange(tableId) {
|
|
1487
|
+
bus.emit({ type: "data-change", tableId: tableId ?? state.selection.tableId });
|
|
1488
|
+
},
|
|
1489
|
+
fireCursor(rowId) {
|
|
1490
|
+
const id = rowId === void 0 ? state.selection.rowId : rowId;
|
|
1491
|
+
bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId: id });
|
|
1492
|
+
},
|
|
1493
|
+
fireOptions() {
|
|
1494
|
+
pushOptions();
|
|
1495
|
+
},
|
|
1496
|
+
fireTheme(message) {
|
|
1497
|
+
bus.emit({ type: "theme-change", message });
|
|
1498
|
+
},
|
|
1499
|
+
emitInitialState() {
|
|
1500
|
+
pushOptions();
|
|
1501
|
+
void pushRecords().then(() => pushRecord(state.selection.rowId));
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
function columnsToRows(columnar) {
|
|
1506
|
+
const cols = Object.keys(columnar);
|
|
1507
|
+
if (cols.length === 0) return [];
|
|
1508
|
+
const length = (columnar[cols[0]] ?? []).length;
|
|
1509
|
+
const rows = [];
|
|
1510
|
+
for (let i = 0; i < length; i++) {
|
|
1511
|
+
const row = {};
|
|
1512
|
+
for (const c of cols) row[c] = columnar[c][i];
|
|
1513
|
+
rows.push(row);
|
|
1514
|
+
}
|
|
1515
|
+
return rows;
|
|
1516
|
+
}
|
|
1517
|
+
function parseFields(fields, _options) {
|
|
1518
|
+
return { ...fields };
|
|
1519
|
+
}
|
|
1520
|
+
function coerceFetchOptions(opts) {
|
|
1521
|
+
if (!opts) return void 0;
|
|
1522
|
+
const format = opts.format === "rows" || opts.format === "columns" ? opts.format : void 0;
|
|
1523
|
+
return { keepEncoded: opts.keepEncoded, format };
|
|
1524
|
+
}
|
|
1525
|
+
function logErr(err) {
|
|
1526
|
+
console.error("[grist-emulator/inline]", err);
|
|
1527
|
+
}
|
|
1528
|
+
function installFakeIframeContext(target) {
|
|
1529
|
+
try {
|
|
1530
|
+
if (target.self !== target.top) return;
|
|
1531
|
+
Object.defineProperty(target, "top", {
|
|
1532
|
+
configurable: true,
|
|
1533
|
+
get: () => ({})
|
|
1534
|
+
});
|
|
1535
|
+
} catch {
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// src/emulator/validators.ts
|
|
1540
|
+
var import_zod2 = require("zod");
|
|
1541
|
+
var emulatorRowSchema = import_zod2.z.object({ id: import_zod2.z.number() }).catchall(import_zod2.z.unknown());
|
|
1542
|
+
var formulaKindSchema = import_zod2.z.enum(["computed", "trigger", "none", "empty_slot"]);
|
|
1543
|
+
var replicaColumnSchema = import_zod2.z.object({
|
|
1544
|
+
colRef: import_zod2.z.number().optional(),
|
|
1545
|
+
type: import_zod2.z.string().optional(),
|
|
1546
|
+
label: import_zod2.z.string().optional(),
|
|
1547
|
+
parentPos: import_zod2.z.number().optional(),
|
|
1548
|
+
isFormula: import_zod2.z.boolean().optional(),
|
|
1549
|
+
formulaKind: formulaKindSchema.optional(),
|
|
1550
|
+
formula: import_zod2.z.string().optional(),
|
|
1551
|
+
description: import_zod2.z.string().optional(),
|
|
1552
|
+
widgetOptions: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
|
|
1553
|
+
displayColRef: import_zod2.z.number().optional(),
|
|
1554
|
+
visibleColRef: import_zod2.z.number().optional()
|
|
1555
|
+
}).strict();
|
|
1556
|
+
var replicaTableSchema = import_zod2.z.object({
|
|
1557
|
+
label: import_zod2.z.string().optional(),
|
|
1558
|
+
columns: import_zod2.z.record(import_zod2.z.string(), replicaColumnSchema).optional(),
|
|
1559
|
+
rows: import_zod2.z.array(emulatorRowSchema).optional(),
|
|
1560
|
+
rowCount: import_zod2.z.number().optional(),
|
|
1561
|
+
dataOmitted: import_zod2.z.boolean().optional(),
|
|
1562
|
+
dataOmittedReason: import_zod2.z.string().optional(),
|
|
1563
|
+
error: import_zod2.z.string().optional()
|
|
1564
|
+
}).strict();
|
|
1565
|
+
var replicaSelectionSchema = import_zod2.z.object({
|
|
1566
|
+
tableId: import_zod2.z.string().optional(),
|
|
1567
|
+
rowId: import_zod2.z.union([import_zod2.z.number(), import_zod2.z.literal("new"), import_zod2.z.null()]).optional(),
|
|
1568
|
+
mappings: import_zod2.z.unknown().nullable().optional(),
|
|
1569
|
+
cursor: import_zod2.z.unknown().optional()
|
|
1570
|
+
}).strict();
|
|
1571
|
+
var replicaWidgetConfigurationSchema = import_zod2.z.object({
|
|
1572
|
+
requiredAccess: import_zod2.z.string().optional(),
|
|
1573
|
+
columns: import_zod2.z.unknown().optional(),
|
|
1574
|
+
allowSelectBy: import_zod2.z.boolean().optional(),
|
|
1575
|
+
hasCustomOptions: import_zod2.z.boolean().optional()
|
|
1576
|
+
}).strict();
|
|
1577
|
+
var replicaAccessTokenSchema = import_zod2.z.object({
|
|
1578
|
+
token: import_zod2.z.string(),
|
|
1579
|
+
baseUrl: import_zod2.z.string(),
|
|
1580
|
+
ttlMsecs: import_zod2.z.number()
|
|
1581
|
+
}).strict();
|
|
1582
|
+
var replicaDocumentSchema = import_zod2.z.object({
|
|
1583
|
+
generatedAt: import_zod2.z.string(),
|
|
1584
|
+
source: import_zod2.z.string().optional(),
|
|
1585
|
+
docName: import_zod2.z.string().optional(),
|
|
1586
|
+
mode: import_zod2.z.enum(["schema-only", "schema+samples", "schema+data"]).optional(),
|
|
1587
|
+
tables: import_zod2.z.record(import_zod2.z.string(), replicaTableSchema),
|
|
1588
|
+
selection: replicaSelectionSchema.optional(),
|
|
1589
|
+
widgetOptions: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
|
|
1590
|
+
widgetInteraction: import_zod2.z.unknown().optional(),
|
|
1591
|
+
configuration: replicaWidgetConfigurationSchema.optional(),
|
|
1592
|
+
accessToken: replicaAccessTokenSchema.nullable().optional()
|
|
1593
|
+
}).strict().refine((value) => Object.keys(value.tables).length > 0, {
|
|
1594
|
+
message: "at least one table is required",
|
|
1595
|
+
path: ["tables"]
|
|
1596
|
+
});
|
|
1597
|
+
function formatZodError(prefix, error) {
|
|
1598
|
+
const details = error.issues.map((issue) => {
|
|
1599
|
+
const where = issue.path.length > 0 ? issue.path.join(".") : "<root>";
|
|
1600
|
+
return `${where}: ${issue.message}`;
|
|
1601
|
+
}).join("; ");
|
|
1602
|
+
return new Error(`${prefix}: ${details}`);
|
|
1603
|
+
}
|
|
1604
|
+
function safeParseDocument(input) {
|
|
1605
|
+
return replicaDocumentSchema.safeParse(input);
|
|
1606
|
+
}
|
|
1607
|
+
function parseDocument(input) {
|
|
1608
|
+
const result = safeParseDocument(input);
|
|
1609
|
+
if (!result.success) {
|
|
1610
|
+
throw formatZodError("parseDocument", result.error);
|
|
1611
|
+
}
|
|
1612
|
+
return result.data;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// src/emulator/create-grist-emulator.ts
|
|
1616
|
+
function createGristEmulator(options) {
|
|
1617
|
+
const validatedDocument = parseDocument(options.document);
|
|
1618
|
+
const state = createDocument({
|
|
1619
|
+
...validatedDocument,
|
|
1620
|
+
widgetOptions: options.widgetOptions ?? validatedDocument.widgetOptions,
|
|
1621
|
+
widgetInteraction: options.widgetInteraction ?? validatedDocument.widgetInteraction ?? { access_level: "read table" }
|
|
1622
|
+
});
|
|
1623
|
+
const bus = createEventBus();
|
|
1624
|
+
const api = createApiBundle(state, bus);
|
|
1625
|
+
const transportKind = options.transport?.kind ?? "inline";
|
|
1626
|
+
let transport;
|
|
1627
|
+
let grist;
|
|
1628
|
+
if (transportKind === "iframe") {
|
|
1629
|
+
if (options.transport?.kind !== "iframe") throw new Error("transport mismatch");
|
|
1630
|
+
transport = createIframeTransport({
|
|
1631
|
+
iframe: options.transport.iframe,
|
|
1632
|
+
targetOrigin: options.transport.targetOrigin,
|
|
1633
|
+
state,
|
|
1634
|
+
bus,
|
|
1635
|
+
api,
|
|
1636
|
+
initialTheme: options.initialTheme
|
|
1637
|
+
});
|
|
1638
|
+
} else {
|
|
1639
|
+
const inline = createInlineTransport({
|
|
1640
|
+
state,
|
|
1641
|
+
bus,
|
|
1642
|
+
api,
|
|
1643
|
+
initialTheme: options.initialTheme,
|
|
1644
|
+
target: options.transport?.kind === "inline" ? options.transport.target : void 0
|
|
1645
|
+
});
|
|
1646
|
+
transport = inline;
|
|
1647
|
+
grist = inline.grist;
|
|
1648
|
+
}
|
|
1649
|
+
function apply(actions) {
|
|
1650
|
+
return applyUserActions(state, bus, actions);
|
|
1651
|
+
}
|
|
1652
|
+
return {
|
|
1653
|
+
state,
|
|
1654
|
+
bus,
|
|
1655
|
+
api,
|
|
1656
|
+
transport,
|
|
1657
|
+
grist,
|
|
1658
|
+
apply,
|
|
1659
|
+
mutate: {
|
|
1660
|
+
addRow: (tableId, fields) => apply([["AddRecord", tableId, null, fields]]).retValues[0],
|
|
1661
|
+
updateRow: (tableId, rowId, fields) => {
|
|
1662
|
+
apply([["UpdateRecord", tableId, rowId, fields]]);
|
|
1663
|
+
},
|
|
1664
|
+
removeRow: (tableId, rowId) => {
|
|
1665
|
+
apply([["RemoveRecord", tableId, rowId]]);
|
|
1666
|
+
}
|
|
1667
|
+
},
|
|
1668
|
+
selectRow(rowId) {
|
|
1669
|
+
state.selection.rowId = rowId;
|
|
1670
|
+
bus.emit({ type: "cursor-change", tableId: state.selection.tableId, rowId });
|
|
1671
|
+
},
|
|
1672
|
+
selectTable(tableId) {
|
|
1673
|
+
if (!state.tables.has(tableId)) throw new Error(`Unknown table: ${tableId}`);
|
|
1674
|
+
state.selection.tableId = tableId;
|
|
1675
|
+
const first = state.tables.get(tableId).rows[0]?.id ?? null;
|
|
1676
|
+
state.selection.rowId = first;
|
|
1677
|
+
bus.emit({ type: "data-change", tableId });
|
|
1678
|
+
bus.emit({ type: "cursor-change", tableId, rowId: first });
|
|
1679
|
+
},
|
|
1680
|
+
setColumnMappings(mappings) {
|
|
1681
|
+
if (mappings === void 0) {
|
|
1682
|
+
delete state.selection.mappings;
|
|
1683
|
+
} else {
|
|
1684
|
+
state.selection.mappings = mappings;
|
|
1685
|
+
}
|
|
1686
|
+
bus.emit({ type: "mappings-change", mappings: resolveMappings(state) });
|
|
1687
|
+
},
|
|
1688
|
+
async setOptions(opts) {
|
|
1689
|
+
await api.WidgetAPI.setOptions(opts);
|
|
1690
|
+
},
|
|
1691
|
+
setInteraction(interaction) {
|
|
1692
|
+
state.widgetInteraction = { ...interaction };
|
|
1693
|
+
bus.emit({ type: "options-change", options: { ...state.widgetOptions }, interaction: { ...interaction } });
|
|
1694
|
+
},
|
|
1695
|
+
theme: {
|
|
1696
|
+
set(next) {
|
|
1697
|
+
bus.emit({ type: "theme-change", message: buildGristThemeHostMessage(next) });
|
|
1698
|
+
}
|
|
1699
|
+
},
|
|
1700
|
+
dispose() {
|
|
1701
|
+
transport.dispose();
|
|
1702
|
+
bus.clear();
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/emulator/templates.ts
|
|
1708
|
+
var PRESET_GENERATED_AT = "1970-01-01T00:00:00.000Z";
|
|
1709
|
+
function replicaBase(overrides) {
|
|
1710
|
+
return {
|
|
1711
|
+
generatedAt: PRESET_GENERATED_AT,
|
|
1712
|
+
...overrides
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
var presets = {
|
|
1716
|
+
/**
|
|
1717
|
+
* Default starter fixture for demos / sandboxes that need a neutral document
|
|
1718
|
+
* without choosing a domain-specific preset.
|
|
1719
|
+
*/
|
|
1720
|
+
defaultTemplate() {
|
|
1721
|
+
return replicaBase({
|
|
1722
|
+
docName: "Default",
|
|
1723
|
+
tables: {
|
|
1724
|
+
Table1: {
|
|
1725
|
+
label: "Table1",
|
|
1726
|
+
rows: [
|
|
1727
|
+
{ id: 1, Name: "Alice" },
|
|
1728
|
+
{ id: 2, Name: "Bob" }
|
|
1729
|
+
]
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
},
|
|
1734
|
+
blank() {
|
|
1735
|
+
return replicaBase({
|
|
1736
|
+
docName: "Blank",
|
|
1737
|
+
tables: {
|
|
1738
|
+
Table1: { rows: [{ id: 1 }] }
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
},
|
|
1742
|
+
todoList() {
|
|
1743
|
+
return replicaBase({
|
|
1744
|
+
docName: "Todo list",
|
|
1745
|
+
mode: "schema+data",
|
|
1746
|
+
tables: {
|
|
1747
|
+
Tasks: {
|
|
1748
|
+
label: "Tasks",
|
|
1749
|
+
columns: {
|
|
1750
|
+
title: { type: "Text", label: "Title" },
|
|
1751
|
+
done: { type: "Bool", label: "Done" },
|
|
1752
|
+
priority: { type: "Choice", label: "Priority" }
|
|
1753
|
+
},
|
|
1754
|
+
rows: [
|
|
1755
|
+
{ id: 1, title: "Buy milk", done: false, priority: "high" },
|
|
1756
|
+
{ id: 2, title: "Write the SOTA emulator", done: true, priority: "high" },
|
|
1757
|
+
{ id: 3, title: "Ship it", done: false, priority: "medium" }
|
|
1758
|
+
]
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
},
|
|
1763
|
+
contacts() {
|
|
1764
|
+
return replicaBase({
|
|
1765
|
+
docName: "Contacts",
|
|
1766
|
+
mode: "schema+data",
|
|
1767
|
+
tables: {
|
|
1768
|
+
Contacts: {
|
|
1769
|
+
label: "Contacts",
|
|
1770
|
+
columns: {
|
|
1771
|
+
firstName: { type: "Text" },
|
|
1772
|
+
lastName: { type: "Text" },
|
|
1773
|
+
email: { type: "Text" },
|
|
1774
|
+
company: { type: "Text" }
|
|
1775
|
+
},
|
|
1776
|
+
rows: [
|
|
1777
|
+
{ id: 1, firstName: "Ada", lastName: "Lovelace", email: "ada@analytical.engine", company: "British Crown" },
|
|
1778
|
+
{ id: 2, firstName: "Linus", lastName: "Torvalds", email: "linus@kernel.org", company: "Linux Foundation" },
|
|
1779
|
+
{ id: 3, firstName: "Margaret", lastName: "Hamilton", email: "margaret@apollo.nasa", company: "NASA" }
|
|
1780
|
+
]
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1787
|
+
0 && (module.exports = {
|
|
1788
|
+
EmulatorActionError,
|
|
1789
|
+
createGristEmulator,
|
|
1790
|
+
enrichEmulatorColumnMetadata,
|
|
1791
|
+
parseDocument,
|
|
1792
|
+
presets,
|
|
1793
|
+
resolveMappings,
|
|
1794
|
+
safeParseDocument
|
|
1795
|
+
});
|
|
1796
|
+
//# sourceMappingURL=emulator.cjs.map
|