grist-widget-sdk 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +79 -208
  3. package/dist/app.cjs +183 -65
  4. package/dist/app.cjs.map +1 -1
  5. package/dist/app.d.cts +5 -100
  6. package/dist/app.d.ts +5 -100
  7. package/dist/app.js +4 -2
  8. package/dist/{chunk-P6VTEKWO.js → chunk-LSALXIGM.js} +82 -10
  9. package/dist/chunk-LSALXIGM.js.map +1 -0
  10. package/dist/{chunk-O7NY46QY.js → chunk-MAPAWVQC.js} +88 -20
  11. package/dist/chunk-MAPAWVQC.js.map +1 -0
  12. package/dist/{chunk-B5ZZNLVS.js → chunk-OXA2VITC.js} +5 -1
  13. package/dist/chunk-OXA2VITC.js.map +1 -0
  14. package/dist/{use-grist-BK0eke9K.d.cts → define-widget-B1FIvI-h.d.cts} +179 -104
  15. package/dist/{use-grist-C5DMZ3wU.d.ts → define-widget-B9gEtJP0.d.ts} +179 -104
  16. package/dist/emulator/testing.cjs +2390 -2243
  17. package/dist/emulator/testing.cjs.map +1 -1
  18. package/dist/emulator/testing.d.cts +27 -9
  19. package/dist/emulator/testing.d.ts +27 -9
  20. package/dist/emulator/testing.js +74 -55
  21. package/dist/emulator/testing.js.map +1 -1
  22. package/dist/emulator.cjs +4 -0
  23. package/dist/emulator.cjs.map +1 -1
  24. package/dist/emulator.d.cts +3 -3
  25. package/dist/emulator.d.ts +3 -3
  26. package/dist/emulator.js +1 -1
  27. package/dist/index.cjs +133 -67
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +5 -1
  32. package/dist/mount-grist-widget-CkB-3iG_.d.ts +137 -0
  33. package/dist/mount-grist-widget-Dtelqao_.d.cts +137 -0
  34. package/package.json +1 -1
  35. package/dist/chunk-B5ZZNLVS.js.map +0 -1
  36. package/dist/chunk-O7NY46QY.js.map +0 -1
  37. package/dist/chunk-P6VTEKWO.js.map +0 -1
@@ -96,9 +96,9 @@ declare class EmulatorActionError extends Error {
96
96
  /**
97
97
  * Resolves the `mappings` payload that `CustomSectionAPI.mappings()` returns.
98
98
  *
99
- * The playground emulator does **not** infer logical → real columns from
100
- * names. Mappings come only from `state.selection.mappings` (playground
101
- * document inspector or tests). When unset, returns `null`.
99
+ * The emulator does **not** infer logical → real columns from names, as
100
+ * real Grist doesn't: mappings come only from `state.selection.mappings`
101
+ * (a test's `emulator.setColumnMappings(...)`). When unset, returns `null`.
102
102
  */
103
103
 
104
104
  declare function resolveMappings(state: EmulatorDocumentState): GristWidgetColumnMap | null;
@@ -96,9 +96,9 @@ declare class EmulatorActionError extends Error {
96
96
  /**
97
97
  * Resolves the `mappings` payload that `CustomSectionAPI.mappings()` returns.
98
98
  *
99
- * The playground emulator does **not** infer logical → real columns from
100
- * names. Mappings come only from `state.selection.mappings` (playground
101
- * document inspector or tests). When unset, returns `null`.
99
+ * The emulator does **not** infer logical → real columns from names, as
100
+ * real Grist doesn't: mappings come only from `state.selection.mappings`
101
+ * (a test's `emulator.setColumnMappings(...)`). When unset, returns `null`.
102
102
  */
103
103
 
104
104
  declare function resolveMappings(state: EmulatorDocumentState): GristWidgetColumnMap | null;
package/dist/emulator.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  presets,
7
7
  resolveMappings,
8
8
  safeParseDocument
9
- } from "./chunk-B5ZZNLVS.js";
9
+ } from "./chunk-OXA2VITC.js";
10
10
  import "./chunk-4ZGVKGDF.js";
11
11
  export {
12
12
  EmulatorActionError,
package/dist/index.cjs CHANGED
@@ -24,6 +24,8 @@ __export(src_exports, {
24
24
  GristWidgetProvider: () => GristWidgetProvider,
25
25
  checkGristCell: () => checkGristCell,
26
26
  decodeGristValue: () => decodeGristValue,
27
+ defineGristWidget: () => defineGristWidget,
28
+ encodeGristCell: () => encodeGristCell,
27
29
  encodeGristValue: () => encodeGristValue,
28
30
  extractGristAttachmentId: () => extractGristAttachmentId,
29
31
  extractGristAttachmentIdsFromCell: () => extractGristAttachmentIdsFromCell,
@@ -858,16 +860,24 @@ function createGristApi(runtime) {
858
860
  throw err;
859
861
  }
860
862
  };
861
- const write = async (fn) => {
863
+ let running = 0;
864
+ let failure = null;
865
+ const write = (fn) => {
866
+ if (running++ === 0) failure = null;
862
867
  runtime.update({ actionStatus: "running", actionError: null });
863
- try {
864
- const out = await fn();
865
- runtime.update({ actionStatus: "idle" });
866
- return out;
867
- } catch (err) {
868
- runtime.update({ actionStatus: "error", actionError: message(err) });
869
- throw err;
870
- }
868
+ const done = (async () => {
869
+ try {
870
+ return await fn();
871
+ } catch (err) {
872
+ failure = message(err);
873
+ throw err;
874
+ } finally {
875
+ if (--running === 0) runtime.update(failure ? { actionStatus: "error", actionError: failure } : { actionStatus: "idle" });
876
+ }
877
+ })();
878
+ done.catch(() => {
879
+ });
880
+ return done;
871
881
  };
872
882
  const tableOps = (resolve) => ({
873
883
  getTableId: () => resolve().getTableId(),
@@ -972,58 +982,6 @@ function validateColumnMappings(spec, mappings) {
972
982
  };
973
983
  }
974
984
 
975
- // src/sdk/runtime/derive.ts
976
- var ACCESS_RANK = { none: 0, "read table": 1, full: 2 };
977
- function parseAccess(raw) {
978
- return raw === "none" || raw === "read table" || raw === "full" ? raw : null;
979
- }
980
- function accessLabel(level) {
981
- return level === "full" ? "Full document access" : level === "read table" ? "Read selected table" : "No document access";
982
- }
983
- function accessDenied(state) {
984
- return state.grantedAccess !== null && ACCESS_RANK[state.grantedAccess] < ACCESS_RANK[state.declaredAccess];
985
- }
986
- function deriveStatus(state) {
987
- switch (state.connection) {
988
- case "connecting":
989
- return "booting";
990
- case "not-embedded":
991
- case "unavailable":
992
- return "unavailable";
993
- case "error":
994
- return "error";
995
- case "ready":
996
- return accessDenied(state) ? "error" : "ready";
997
- }
998
- }
999
- function deriveError(state) {
1000
- if (state.connection === "not-embedded") return "This page is not embedded in a Grist document.";
1001
- if (state.connection === "unavailable") return "The Grist plugin API (grist-plugin-api.js) did not load.";
1002
- if (state.connection === "error") return state.connectionError;
1003
- if (state.connection === "ready" && accessDenied(state)) {
1004
- return `Grist granted "${accessLabel(state.grantedAccess)}"; this widget needs "${accessLabel(state.declaredAccess)}". Set the access level in the Custom widget settings.`;
1005
- }
1006
- return null;
1007
- }
1008
- var MAPPINGS_OK = Object.freeze({ ok: true, pending: false, missing: [], emptyMultiples: [] });
1009
- var MAPPINGS_PENDING = Object.freeze({ ok: true, pending: true, missing: [], emptyMultiples: [] });
1010
- function deriveColumnMappingStatus(columns, mappings) {
1011
- if (!columns?.length) return MAPPINGS_OK;
1012
- if (mappings === null) return MAPPINGS_PENDING;
1013
- return validateColumnMappings(columns, mappings);
1014
- }
1015
- function mapBackPatch(patch, mappings) {
1016
- const fields = {};
1017
- const skipped = [];
1018
- for (const [name, value] of Object.entries(patch)) {
1019
- if (value === void 0) continue;
1020
- const real = mappings[name];
1021
- if (Array.isArray(real)) skipped.push(name);
1022
- else fields[typeof real === "string" && real ? real : name] = value;
1023
- }
1024
- return { fields, skipped };
1025
- }
1026
-
1027
985
  // src/sdk/runtime/cells.ts
1028
986
  var ok = (value) => ({ ok: true, value });
1029
987
  var bad = (message2) => ({ ok: false, message: message2 });
@@ -1087,10 +1045,13 @@ function checkOne(base, value) {
1087
1045
  return ok(value);
1088
1046
  }
1089
1047
  }
1048
+ function basesOf(type) {
1049
+ return (type ?? "Any").split(",").map((t) => t.trim().split(":")[0]).filter(Boolean);
1050
+ }
1090
1051
  function checkGristCell(type, value) {
1091
1052
  const error = formulaError(value);
1092
1053
  if (error) return bad(`formula error (${error})`);
1093
- const bases = (type ?? "Any").split(",").map((t) => t.trim().split(":")[0]).filter(Boolean);
1054
+ const bases = basesOf(type);
1094
1055
  if (bases.length === 0 || bases.includes("Any")) return ok(value === void 0 ? null : value);
1095
1056
  let first = null;
1096
1057
  for (const base of bases) {
@@ -1100,6 +1061,19 @@ function checkGristCell(type, value) {
1100
1061
  }
1101
1062
  return first;
1102
1063
  }
1064
+ var LIST_BASES = ["ChoiceList", "RefList", "Attachments"];
1065
+ function encodeGristCell(type, value) {
1066
+ if (value === null || value === void 0) return value;
1067
+ const bases = basesOf(type);
1068
+ const accepts = (...names) => names.some((n) => bases.includes(n));
1069
+ if (value instanceof Date) return accepts("Date", "DateTime") ? Math.floor(value.getTime() / 1e3) : value;
1070
+ if (Array.isArray(value)) {
1071
+ if (!accepts(...LIST_BASES) || value[0] === "L") return value;
1072
+ return ["L", ...value.map((item) => rowIdOf(item) ?? item)];
1073
+ }
1074
+ if (accepts("Ref") && typeof value === "object") return rowIdOf(value) ?? value;
1075
+ return value;
1076
+ }
1103
1077
  var nameOf = (c) => typeof c === "string" ? c : c.name;
1104
1078
  var typeOf = (c) => typeof c === "string" ? void 0 : c.type;
1105
1079
  var isOptional = (c) => typeof c === "object" && c.optional === true;
@@ -1140,6 +1114,62 @@ function mapAndCheck(record, columns, mappings, schema) {
1140
1114
  return { value: out, issues };
1141
1115
  }
1142
1116
 
1117
+ // src/sdk/runtime/derive.ts
1118
+ var ACCESS_RANK = { none: 0, "read table": 1, full: 2 };
1119
+ function parseAccess(raw) {
1120
+ return raw === "none" || raw === "read table" || raw === "full" ? raw : null;
1121
+ }
1122
+ function accessLabel(level) {
1123
+ return level === "full" ? "Full document access" : level === "read table" ? "Read selected table" : "No document access";
1124
+ }
1125
+ function accessDenied(state) {
1126
+ return state.grantedAccess !== null && ACCESS_RANK[state.grantedAccess] < ACCESS_RANK[state.declaredAccess];
1127
+ }
1128
+ function deriveStatus(state) {
1129
+ switch (state.connection) {
1130
+ case "connecting":
1131
+ return "booting";
1132
+ case "not-embedded":
1133
+ case "unavailable":
1134
+ return "unavailable";
1135
+ case "error":
1136
+ return "error";
1137
+ case "ready":
1138
+ return accessDenied(state) ? "error" : "ready";
1139
+ }
1140
+ }
1141
+ function deriveError(state) {
1142
+ if (state.connection === "not-embedded") return "This page is not embedded in a Grist document.";
1143
+ if (state.connection === "unavailable") return "The Grist plugin API (grist-plugin-api.js) did not load.";
1144
+ if (state.connection === "error") return state.connectionError;
1145
+ if (state.connection === "ready" && accessDenied(state)) {
1146
+ return `Grist granted "${accessLabel(state.grantedAccess)}"; this widget needs "${accessLabel(state.declaredAccess)}". Set the access level in the Custom widget settings.`;
1147
+ }
1148
+ return null;
1149
+ }
1150
+ var MAPPINGS_OK = Object.freeze({ ok: true, pending: false, missing: [], emptyMultiples: [] });
1151
+ var MAPPINGS_PENDING = Object.freeze({ ok: true, pending: true, missing: [], emptyMultiples: [] });
1152
+ function deriveColumnMappingStatus(columns, mappings) {
1153
+ if (!columns?.length) return MAPPINGS_OK;
1154
+ if (mappings === null) return MAPPINGS_PENDING;
1155
+ return validateColumnMappings(columns, mappings);
1156
+ }
1157
+ function mapBackPatch(patch, mappings, columns) {
1158
+ const fields = {};
1159
+ const skipped = [];
1160
+ for (const [name, value] of Object.entries(patch)) {
1161
+ if (value === void 0) continue;
1162
+ const real = mappings[name];
1163
+ if (Array.isArray(real)) {
1164
+ skipped.push(name);
1165
+ continue;
1166
+ }
1167
+ const declared = columns?.find((c) => nameOf(c) === name);
1168
+ fields[typeof real === "string" && real ? real : name] = declared ? encodeGristCell(typeOf(declared), value) : value;
1169
+ }
1170
+ return { fields, skipped };
1171
+ }
1172
+
1143
1173
  // src/sdk/helpers/grist-widget-interaction.ts
1144
1174
  function parseGristWidgetLinkingInfo(raw) {
1145
1175
  if (!raw || typeof raw !== "object" || !("asTarget" in raw)) return void 0;
@@ -1489,7 +1519,27 @@ function useMapped(state, options) {
1489
1519
  return { record: selected.value, records: mappedRecords, issues: issues.length ? issues : NO_ISSUES };
1490
1520
  }, [record, records, mappings, columns, schema]);
1491
1521
  }
1492
- function buildResult(state, mapped, connection, mapBack, resolveMappedColumnId) {
1522
+ function recordWrites(connection) {
1523
+ const { runtime, api } = connection;
1524
+ const fieldsOf = (patch) => {
1525
+ const { id: _id, ...rest } = patch;
1526
+ return mapBackPatch(rest, runtime.getState().mappings ?? EMPTY_MAPPINGS, runtime.options.columns).fields;
1527
+ };
1528
+ const updateRecord = ((a, b) => {
1529
+ const rowId = typeof a === "number" ? a : runtime.getState().record?.id;
1530
+ const patch = typeof a === "number" ? b ?? {} : a;
1531
+ if (typeof rowId !== "number") {
1532
+ return Promise.reject(new Error("updateRecord(): no row is selected. Pass the row id: updateRecord(rowId, patch)."));
1533
+ }
1534
+ return api.table.update({ id: rowId, fields: fieldsOf(patch) });
1535
+ });
1536
+ const addRecord = async (patch) => {
1537
+ const created = await api.table.create({ fields: fieldsOf(patch) });
1538
+ return (Array.isArray(created) ? created[0] : created).id;
1539
+ };
1540
+ return { updateRecord, addRecord };
1541
+ }
1542
+ function buildResult(state, mapped, connection, mapBack, resolveMappedColumnId, writes) {
1493
1543
  const { runtime, api } = connection;
1494
1544
  const status = deriveStatus(state);
1495
1545
  const columns = runtime.options.columns;
@@ -1510,6 +1560,8 @@ function buildResult(state, mapped, connection, mapBack, resolveMappedColumnId)
1510
1560
  mode: state.isNewRecord ? "new-row" : state.record ? "row" : "empty",
1511
1561
  resolveMappedColumnId,
1512
1562
  mapBack,
1563
+ updateRecord: writes.updateRecord,
1564
+ addRecord: writes.addRecord,
1513
1565
  currentTableId: state.currentTableId,
1514
1566
  actionStatus: state.actionStatus,
1515
1567
  actionError: state.actionError,
@@ -1532,7 +1584,7 @@ function useGristResult(connection) {
1532
1584
  );
1533
1585
  const mapBack = (0, import_react.useCallback)(
1534
1586
  (patch) => {
1535
- const { fields, skipped } = mapBackPatch(patch, mappings);
1587
+ const { fields, skipped } = mapBackPatch(patch, mappings, runtime.options.columns);
1536
1588
  const previous = runtime.getState().mapBackSkipped;
1537
1589
  if (previous.join("\0") !== skipped.join("\0")) runtime.update({ mapBackSkipped: skipped });
1538
1590
  return fields;
@@ -1540,9 +1592,10 @@ function useGristResult(connection) {
1540
1592
  [mappings, runtime]
1541
1593
  );
1542
1594
  const mapped = useMapped(state, runtime.options);
1595
+ const writes = (0, import_react.useMemo)(() => recordWrites(connection), [connection]);
1543
1596
  return (0, import_react.useMemo)(
1544
- () => buildResult(state, mapped, connection, mapBack, resolveMappedColumnId),
1545
- [state, mapped, connection, mapBack, resolveMappedColumnId]
1597
+ () => buildResult(state, mapped, connection, mapBack, resolveMappedColumnId, writes),
1598
+ [state, mapped, connection, mapBack, resolveMappedColumnId, writes]
1546
1599
  );
1547
1600
  }
1548
1601
  function useGrist(options) {
@@ -1578,6 +1631,17 @@ var DETACHED = (() => {
1578
1631
  return { runtime, api: createGristApi(runtime) };
1579
1632
  })();
1580
1633
 
1634
+ // src/sdk/define-widget.ts
1635
+ function defineGristWidget(definition) {
1636
+ const { title, description, ...options } = definition;
1637
+ return {
1638
+ definition,
1639
+ options,
1640
+ metadata: { title, description },
1641
+ useGrist: () => useGrist(options)
1642
+ };
1643
+ }
1644
+
1581
1645
  // src/sdk/components/grist-widget-provider.tsx
1582
1646
  var import_react2 = require("react");
1583
1647
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -1805,9 +1869,9 @@ function useGristSchema(options) {
1805
1869
  const [document2, setDocument] = (0, import_react4.useState)(null);
1806
1870
  const [loading, setLoading] = (0, import_react4.useState)(false);
1807
1871
  const [error, setError] = (0, import_react4.useState)(null);
1808
- const includeRowsForBuild = replicaRowMode === "schema-only" ? false : replicaRowMode === "schema+data" ? "full" : "samples";
1809
1872
  const load = (0, import_react4.useCallback)(async () => {
1810
1873
  if (!docApi) return;
1874
+ const includeRowsForBuild = replicaRowMode === "schema-only" ? false : replicaRowMode === "schema+data" ? "full" : "samples";
1811
1875
  setLoading(true);
1812
1876
  setError(null);
1813
1877
  try {
@@ -2001,6 +2065,8 @@ function gristAddTableAction(tableId, tableInfo) {
2001
2065
  GristWidgetProvider,
2002
2066
  checkGristCell,
2003
2067
  decodeGristValue,
2068
+ defineGristWidget,
2069
+ encodeGristCell,
2004
2070
  encodeGristValue,
2005
2071
  extractGristAttachmentId,
2006
2072
  extractGristAttachmentIdsFromCell,