grist-widget-sdk 0.3.27 → 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.
- package/CHANGELOG.md +84 -0
- package/README.md +79 -208
- package/dist/app.cjs +1336 -4415
- package/dist/app.cjs.map +1 -1
- package/dist/app.d.cts +6 -103
- package/dist/app.d.ts +6 -103
- package/dist/app.js +5 -7
- package/dist/chunk-4ZGVKGDF.js +204 -0
- package/dist/chunk-4ZGVKGDF.js.map +1 -0
- package/dist/{chunk-6Y2IONGJ.js → chunk-LSALXIGM.js} +191 -20
- package/dist/chunk-LSALXIGM.js.map +1 -0
- package/dist/chunk-MAPAWVQC.js +1898 -0
- package/dist/chunk-MAPAWVQC.js.map +1 -0
- package/dist/{chunk-YQNMIL33.js → chunk-OXA2VITC.js} +532 -181
- package/dist/chunk-OXA2VITC.js.map +1 -0
- package/dist/define-widget-B1FIvI-h.d.cts +433 -0
- package/dist/define-widget-B9gEtJP0.d.ts +433 -0
- package/dist/emulator/testing.cjs +3341 -6512
- package/dist/emulator/testing.cjs.map +1 -1
- package/dist/emulator/testing.d.cts +29 -14
- package/dist/emulator/testing.d.ts +29 -14
- package/dist/emulator/testing.js +78 -72
- package/dist/emulator/testing.js.map +1 -1
- package/dist/emulator.cjs +63 -249
- package/dist/emulator.cjs.map +1 -1
- package/dist/emulator.d.cts +6 -8
- package/dist/emulator.d.ts +6 -8
- package/dist/emulator.js +5 -11
- package/dist/index.cjs +1442 -4862
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -260
- package/dist/index.d.ts +81 -260
- package/dist/index.js +12 -158
- package/dist/mount-grist-widget-CkB-3iG_.d.ts +137 -0
- package/dist/mount-grist-widget-Dtelqao_.d.cts +137 -0
- package/dist/{inline-DpcT8tV9.d.ts → templates-CGmRIQBV.d.cts} +89 -35
- package/dist/{inline-C7DkvuET.d.cts → templates-Cq94qOcf.d.ts} +89 -35
- package/dist/{types-D3JxUCfL.d.cts → types-DBStn0Dk.d.cts} +2 -26
- package/dist/{types-D3JxUCfL.d.ts → types-DBStn0Dk.d.ts} +2 -26
- package/package.json +8 -37
- package/dist/advanced.cjs +0 -2799
- package/dist/advanced.cjs.map +0 -1
- package/dist/advanced.d.cts +0 -822
- package/dist/advanced.d.ts +0 -822
- package/dist/advanced.js +0 -872
- package/dist/advanced.js.map +0 -1
- package/dist/chunk-22U7WAY3.js +0 -463
- package/dist/chunk-22U7WAY3.js.map +0 -1
- package/dist/chunk-64SVRWW7.js +0 -758
- package/dist/chunk-64SVRWW7.js.map +0 -1
- package/dist/chunk-6Y2IONGJ.js.map +0 -1
- package/dist/chunk-B4AKVSWV.js +0 -2362
- package/dist/chunk-B4AKVSWV.js.map +0 -1
- package/dist/chunk-ELGVOHFD.js +0 -32
- package/dist/chunk-ELGVOHFD.js.map +0 -1
- package/dist/chunk-GTTD5HPO.js +0 -36
- package/dist/chunk-GTTD5HPO.js.map +0 -1
- package/dist/chunk-NO6FYX33.js +0 -2084
- package/dist/chunk-NO6FYX33.js.map +0 -1
- package/dist/chunk-OPTOYD2Y.js +0 -277
- package/dist/chunk-OPTOYD2Y.js.map +0 -1
- package/dist/chunk-PM3B4WYV.js +0 -12
- package/dist/chunk-PM3B4WYV.js.map +0 -1
- package/dist/chunk-REV5AUXD.js +0 -25
- package/dist/chunk-REV5AUXD.js.map +0 -1
- package/dist/chunk-YQNMIL33.js.map +0 -1
- package/dist/create-grist-emulator-BCqp1BF9.d.ts +0 -81
- package/dist/create-grist-emulator-DBM8OSIR.d.cts +0 -81
- package/dist/emulator/devtools.cjs +0 -1404
- package/dist/emulator/devtools.cjs.map +0 -1
- package/dist/emulator/devtools.d.cts +0 -48
- package/dist/emulator/devtools.d.ts +0 -48
- package/dist/emulator/devtools.js +0 -506
- package/dist/emulator/devtools.js.map +0 -1
- package/dist/emulator/transports.cjs +0 -1132
- package/dist/emulator/transports.cjs.map +0 -1
- package/dist/emulator/transports.d.cts +0 -38
- package/dist/emulator/transports.d.ts +0 -38
- package/dist/emulator/transports.js +0 -12
- package/dist/emulator/transports.js.map +0 -1
- package/dist/templates-8k2LKoO-.d.cts +0 -24
- package/dist/templates-DIwi_mvI.d.ts +0 -24
- package/dist/use-grist-reads-DUMvJgV7.d.ts +0 -480
- package/dist/use-grist-reads-DpjVuI0H.d.cts +0 -480
- package/dist/use-grist-types-DdOLEJXN.d.cts +0 -499
- package/dist/use-grist-types-kwxoFeub.d.ts +0 -499
- package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.cts +0 -43
- package/dist/use-grist-widget-options-from-context-CGOlQ5Vc.d.ts +0 -43
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
# grist-widget-sdk
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d3d9232: **`defineGristWidget()`: one declaration types, mounts and tests a widget.**
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export const widget = defineGristWidget({
|
|
11
|
+
title: "Tasks",
|
|
12
|
+
requiredAccess: "full",
|
|
13
|
+
columns: [{ name: "Title", type: "Text" }, { name: "Done", type: "Bool" }],
|
|
14
|
+
})
|
|
15
|
+
const w = widget.useGrist() // mappedRecord typed from the declaration: no generics, no `as const`
|
|
16
|
+
await w.updateRecord({ Done: true }) // the selected row, encoded for each declared type
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- `defineGristWidget(definition)` keeps the column names and types literal (a `const` type parameter), so `widget.useGrist()` types `mappedRecord`, `mappedRecords`, `mapBack` and the writes from the declaration. `GristWidgetRow<typeof widget>` names the record type.
|
|
20
|
+
- `useGrist()` gains `updateRecord(patch)` / `updateRecord(rowId, patch)` and `addRecord(patch)`: `mapBack` + encoding + `table.update` / `table.create` in one call, under the declared names.
|
|
21
|
+
- `mountGristWidget(entry, { dev })` takes the widget's entry module (`import * as entry from "./app/App"`) and reads its exports: `default`, `widget` (or `GRIST_OPTIONS` and `WIDGET_METADATA`), and optionally `Alerts`, `LocalDevPage`, `ReleasePage`. A widget's customisations then live in its own file instead of `src/main.tsx`. The declaration's title and description set the page's title and meta tags. `mountGristWidget({ App, options, dev })` still works.
|
|
22
|
+
- `renderWithGrist(<App />, { widget, rows })` runs the widget as `mountGristWidget()` does inside Grist, against a table with a column per declared column, every one mapped, seeded with typed rows: no document or mapping setup in a test. `smokeTestWidget(entry)` takes the entry module as `mountGristWidget()` does (or `{ App, widget }`), so one smoke test fits a widget declared either way.
|
|
23
|
+
|
|
24
|
+
Nothing changes for an existing widget: `GRIST_OPTIONS`, `useGrist<TRow, TMapped>()`, `mapBack` and `table.update` work as before.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 734d307: **`pnpm dev` opens the widget in real Grist, in the browser, with no Docker.** The template ships its demo page (`public/demo/`, a document for its widget and the page that opens it in grist-static), and on the dev server `http://localhost:5173/demo/index.html` runs that Grist on the dev server's own widget: edits reload in place, writes land in the document. The SDK's local-dev page links to it (or says how to get it, `pnpm demo --save`, when the widget has no demo page yet). The scaffold puts the widget's title in the page.
|
|
29
|
+
|
|
30
|
+
Existing widgets can do nothing: those with a demo page (`pnpm demo --save`) already get it on `pnpm dev` at `/demo/index.html`; the others get it once they run `pnpm demo --save` (Docker, once).
|
|
31
|
+
- dfb2e66: **Writes are the mirror of typed reads.** `mapBack` now encodes each value for the type its column declares: a ChoiceList's `string[]`, a RefList's row ids or an Attachments list become `["L", ...]`, a `Date` becomes seconds, a `Reference` its row id. Before, writing back what `mappedRecord` gave stored a ChoiceList or RefList cell as an error (`["E", "KeyError"]`, checked in Grist 1.7.22), because Grist's plugin API writes values as given. `encodeGristCell(type, value)` is exported for writes that don't go through `mapBack`; a value already encoded passes through.
|
|
32
|
+
|
|
33
|
+
The emulator now stores a bare list written to a list column as Grist does, an error cell reported in `cellIssues`, so `pnpm test` shows the mistake instead of hiding it.
|
|
34
|
+
|
|
35
|
+
Overlapping writes count as one run: `actionStatus` stays `"running"` until the last one ends, then is `"error"` if any failed (the first write to end used to set it back to `"idle"`). A failed write nobody awaits no longer logs an unhandled rejection; the alert reports it, and the promise still rejects for whoever awaits it.
|
|
36
|
+
|
|
37
|
+
What to change, if a widget relies on it: a widget that already encoded lists itself (`["L", ...]`, `encodeGristValue`, the attachment helpers) needs nothing. A test that wrote a bare array to a RefList through the emulator now gets an error cell: encode it, or go through `mapBack`.
|
|
38
|
+
- ca3f010: `GristMappedRow` now types every declared column as its value or `null`, `Text` and `Bool` included (`Title: string | null`, not `string`). It promised a non-null value for those types while `mappedRecord` holds `null` for a cell that doesn't match its declared type, so a widget trusting the type could crash on a formula error. A widget that reads a `Text` or `Bool` column from a `GristMappedRow` may need a fallback (`row.Title ?? ""`) to compile.
|
|
39
|
+
|
|
40
|
+
The package README, shown on npm, describes the 0.4.0 surface; it still listed `grist-widget-sdk/advanced`, the slice hooks and the emulator's iframe transport and devtools, all removed in 0.4.0.
|
|
41
|
+
|
|
42
|
+
## 0.4.0
|
|
43
|
+
|
|
44
|
+
### Minor Changes
|
|
45
|
+
|
|
46
|
+
- a257767: The emulator is a test harness only. Removed: `grist-widget-sdk/emulator/transports` (the iframe transport, `createIframeTransport`) and `grist-widget-sdk/emulator/devtools` (the inspector), which only the retired playground used; `createGristEmulator`'s `transport` option takes `{ kind: "inline", target? }` only. `renderWithGrist`, `smokeTestWidget` and `createGristEmulator` are unchanged. To see a widget run, use a real Grist: `pnpm demo`, or its demo page (`pnpm demo --save`).
|
|
47
|
+
- 74395a7: **0.4.0: a synchronous runtime and a small public surface (breaking).**
|
|
48
|
+
|
|
49
|
+
The connection to Grist is one synchronous `connect()` and one store read through `useSyncExternalStore`, instead of the handshake state machine (detection polling, a 30 s heartbeat, generations). `useGrist()` keeps its shape for what widgets use and re-renders less (a cursor move: 3 renders to 1). The root entry exports 29 runtime names instead of about 195; `grist-widget-sdk/advanced` is gone.
|
|
50
|
+
|
|
51
|
+
Fixed on the way: `fetchSelectedTable()` returned rows while typed as columns (a widget passing it to `tableDataToRows()` got nothing; it now returns columns); `mode` stuck on `"new-row"` when the selection went away; records reused and mutated by the host no longer go unnoticed. The emulator now matches Grist on `setOptions` (replaces), the add-row (`onNewRecord` only), unmapped columns (`mappedRecord` is `null`) and access (it grants what the widget asks for unless a test sets `widgetInteraction.access_level`).
|
|
52
|
+
|
|
53
|
+
What to replace, if a widget uses it (the six reference widgets need nothing):
|
|
54
|
+
|
|
55
|
+
| Removed | Use instead |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `useGristSelection()`, `useGristStatus()`, `useGristWrites()`, `useGristTheme()` | `useGrist()` |
|
|
58
|
+
| `grist-widget-sdk/advanced` (`useGristReady`, `useGristHandshake`, `useGristCapabilities`, `useGristRowsFromTable`, …), `GristHandshakeProvider` | `useGrist()` (`status`, `columnMappingStatus`), or `fetchTableRows` in an effect |
|
|
59
|
+
| `w.capabilities` | `w.status`, `w.columnMappingStatus.ok`, `w.isReady` |
|
|
60
|
+
| `w.getWidgetOptions()`, `w.getWidgetOption(key)` | `w.widgetOptions` |
|
|
61
|
+
| `w.setWidgetOption(key, value)` | `w.patchWidgetOptions({ [key]: value })` |
|
|
62
|
+
| `w.clearWidgetOptions()` | `w.setWidgetOptions({})` |
|
|
63
|
+
| `w.fetchRow(table, id)` | `w.fetchTableRows(table)` |
|
|
64
|
+
| `w.fetchSelectedRecord(id)` | `w.record`, or `w.fetchSelectedTable()` |
|
|
65
|
+
| `w.buildReplicaDocumentFromDocApi(...)`, `buildReplicaDocumentFromDocApi` | `useGristSchema()` |
|
|
66
|
+
| `w.getDocName()`, `w.configure()`, `w.refreshMappings()`, `w.refreshCurrentTable()`, `w.currentTableLoading` | — (`currentTableId` and `mappings` follow Grist) |
|
|
67
|
+
| `w.isNewRecord`, `w.recordsMappings`, `w.newRecordMappings` | `w.mode === "new-row"`, `w.mappings` |
|
|
68
|
+
| `w.widgetInteraction`, `w.readError`, `w.mapBackSkipped` | the alerts `mountGristWidget()` shows |
|
|
69
|
+
| `w.getAccessToken()`, `w.fetchAttachmentBase64()`, `getGristAccessToken`, `fetchWithGristAuth` | `w.fetchWithAuth()`, `w.fetchAttachmentBlob()` |
|
|
70
|
+
| `<GristBoundary gate widget bootingFallback …>` | `<GristBoundary>`: it gates on declared columns itself |
|
|
71
|
+
| alert and boundary helpers (`getGristSdkAlertDescriptors`, `deriveBoundaryView`, `format…AlertMessage`, …), `validateColumnMappings` | `mountGristWidget()` shows them; `w.columnMappingStatus` |
|
|
72
|
+
| replica builders and normalizers, value codecs beyond `decodeGristValue` / `encodeGristValue` / `isGristCellError`, `gristBulk*` / table / document action builders | write the user-action tuple; `useGristSchema()` |
|
|
73
|
+
| `GristReadyOptions.availabilityMaxAttempts`, `availabilityIntervalMs`, `logWhenNotEmbedded` | — (no polling) |
|
|
74
|
+
- 673a02d: **Typed reads, with read-only access; no zod in a widget's bundle.**
|
|
75
|
+
|
|
76
|
+
Each mapped cell is now checked against the type its column declares in `GRIST_OPTIONS.columns`: no schema to fetch, no full access. `mappedRecord` (and the new `mappedRecords`, every row of the section) holds the value each type promises (`Text` → string, `Numeric` → number, `Date` → `Date`, `Ref` → row id, `RefList` / `Attachments` → ids, `ChoiceList` → strings); a cell that doesn't match (text Grist couldn't parse, a formula error, a column whose type changed) is `null`, and listed in the new `cellIssues` (`{ rowId, column, message, value }`), which `mountGristWidget()` shows as a `cell-issues` alert. `GristMappedRow<typeof GRIST_OPTIONS>` types the mapped records from the declaration (declare it `as const satisfies UseGristOptions`). `UseGristOptions.schema` takes any Standard Schema (zod 4, valibot, arktype) for extra rules; the SDK depends on none. `checkGristCell(type, value)` checks a value read another way.
|
|
77
|
+
|
|
78
|
+
Removed: `safeParseGristTableData`, `safeParseRowsToDisplayRows` and `fetchTableRows`'s `safeParse` option (the zod-based safe-parse), which put zod in every widget. A widget's template build goes from 129 KB to 94 KB of JavaScript, gzipped; CI now fails above 100 KB.
|
|
79
|
+
|
|
80
|
+
What to change, if a widget relies on it (the six reference widgets need nothing): a `Ref` column's value in `mappedRecord` is now its row id (it was a `Reference` object), and a cell that doesn't match its declared type is `null` (it was the raw value); safe-parse users read `mappedRecords` / `cellIssues`, or call `checkGristCell` per cell.
|
|
81
|
+
|
|
82
|
+
### Patch Changes
|
|
83
|
+
|
|
84
|
+
- f4a467a: Routine dependency updates via Dependabot (PR #241). See the PR diff for the exact version bumps.
|
|
85
|
+
- f5e04b4: `fetchWithGristAuth()` (and `w.fetchWithAuth()`) now refuses an absolute URL on another origin than the document's Grist instance, instead of sending the document's access token there. The token travels as the `auth` query parameter, so a URL taken from a cell or another input could hand it to any host. Relative paths and absolute URLs on the Grist instance work as before; for other hosts, use plain `fetch()`.
|
|
86
|
+
|
|
3
87
|
## 0.3.27
|
|
4
88
|
|
|
5
89
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,249 +1,120 @@
|
|
|
1
1
|
# grist-widget-sdk
|
|
2
2
|
|
|
3
|
-
React hooks
|
|
3
|
+
React hooks for [Grist](https://www.getgrist.com) custom widgets: one
|
|
4
|
+
`useGrist()` for the connection, the selected record and the section's
|
|
5
|
+
records, column mapping, typed reads, writes, widget options and the
|
|
6
|
+
document's REST API, plus an emulator to test a widget without Grist.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Documentation: <https://gristwidgets.com/docs>. To start a widget, use the
|
|
9
|
+
scaffold, which wires all of this up:
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npm create grist-widget my-widget
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Why not `grist` directly?
|
|
8
16
|
|
|
9
17
|
| Raw `grist` | SDK |
|
|
10
18
|
| --- | --- |
|
|
11
|
-
| `grist.ready`
|
|
12
|
-
| Wire-format dates
|
|
13
|
-
| `mapColumnNames
|
|
14
|
-
| No test harness | `renderWithGrist`
|
|
19
|
+
| `grist.ready` and `onRecord` callbacks | `useGrist()`: `status`, `record`, `records`, `mode` |
|
|
20
|
+
| Wire-format dates, refs and errors | Each mapped cell as its declared type (`mappedRecord`), mismatches in `cellIssues` |
|
|
21
|
+
| `mapColumnNames`, tuples for writes | `mappedRecord`, `mapBack`, `table.update` |
|
|
22
|
+
| No test harness | `renderWithGrist` with emulator presets |
|
|
15
23
|
|
|
16
24
|
Full comparison: [Raw plugin API vs SDK](https://gristwidgets.com/docs/sdk/guide/raw-plugin-api-vs-sdk).
|
|
17
25
|
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
- Main API via `grist-widget-sdk` (hooks, types, helpers)
|
|
21
|
-
- `grist-widget-sdk/advanced` — lower-level hooks (`useGristReady`, `useGristHandshake`, …)
|
|
22
|
-
- `grist-widget-sdk/emulator` subpath for emulator utilities
|
|
23
|
-
- ESM + CJS + type declaration builds (`.d.ts`)
|
|
24
|
-
|
|
25
|
-
## Installation
|
|
26
|
+
## Install
|
|
26
27
|
|
|
27
28
|
```bash
|
|
28
29
|
pnpm add grist-widget-sdk
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- `react >= 18`
|
|
34
|
-
- `react-dom >= 18`
|
|
35
|
-
|
|
36
|
-
## Grist Runtime Script (Required)
|
|
37
|
-
|
|
38
|
-
This package expects a global `grist` object at runtime.
|
|
39
|
-
|
|
40
|
-
Load the official Grist plugin API script in your app shell/layout:
|
|
32
|
+
Peer dependencies: `react` and `react-dom` 18 or later. The page must load
|
|
33
|
+
Grist's plugin API, which defines the `grist` global:
|
|
41
34
|
|
|
42
35
|
```html
|
|
43
36
|
<script src="https://docs.getgrist.com/grist-plugin-api.js"></script>
|
|
44
37
|
```
|
|
45
38
|
|
|
46
|
-
##
|
|
39
|
+
## A widget
|
|
47
40
|
|
|
48
41
|
```tsx
|
|
49
|
-
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
</body>
|
|
63
|
-
</html>
|
|
64
|
-
)
|
|
42
|
+
// src/app/App.tsx
|
|
43
|
+
import { defineGristWidget } from "grist-widget-sdk"
|
|
44
|
+
|
|
45
|
+
export const widget = defineGristWidget({
|
|
46
|
+
title: "Tasks",
|
|
47
|
+
requiredAccess: "full",
|
|
48
|
+
columns: [{ name: "Title", type: "Text" }, { name: "Done", type: "Bool" }],
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export default function App() {
|
|
52
|
+
const w = widget.useGrist() // w.mappedRecord: { id; Title: string | null; Done: boolean | null } | null
|
|
53
|
+
if (w.mode !== "row" || !w.mappedRecord) return <p>Select a task.</p>
|
|
54
|
+
return <button onClick={() => w.updateRecord({ Done: true })}>{w.mappedRecord.Title}</button>
|
|
65
55
|
}
|
|
66
56
|
```
|
|
67
57
|
|
|
68
|
-
Then render widget content from your page, for example `app/page.tsx`:
|
|
69
|
-
|
|
70
58
|
```tsx
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const w = useGrist()
|
|
75
|
-
|
|
76
|
-
if (w.status === "booting") return <p>Loading...</p>
|
|
77
|
-
if (w.mode === "empty") return <p>Select a row.</p>
|
|
78
|
-
if (w.mode === "new-row") return <p>New row flow</p>
|
|
59
|
+
// src/main.tsx
|
|
60
|
+
import { mountGristWidget } from "grist-widget-sdk/app"
|
|
61
|
+
import * as entry from "./app/App"
|
|
79
62
|
|
|
80
|
-
|
|
81
|
-
}
|
|
63
|
+
mountGristWidget(entry, { dev: import.meta.env.DEV })
|
|
82
64
|
```
|
|
83
65
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
66
|
+
The declaration types everything with no generics: the mapped records, and
|
|
67
|
+
the writes (`updateRecord`, `addRecord`), which encode each value for its
|
|
68
|
+
declared type. `mountGristWidget()` reads `App.tsx`'s exports: the widget,
|
|
69
|
+
its declaration (for the access, the columns and the page title), and any
|
|
70
|
+
page it replaces. Inside Grist it renders the widget in `GristWidgetProvider`
|
|
71
|
+
and `GristBoundary` (the connecting, access and column-mapping screens), with
|
|
72
|
+
the SDK's alerts, in Grist's light or dark theme.
|
|
73
|
+
|
|
74
|
+
## Entry points
|
|
75
|
+
|
|
76
|
+
- **`grist-widget-sdk`**: `defineGristWidget`, `useGrist`, `useGristContext`, `useGristOptional`,
|
|
77
|
+
`GristWidgetProvider`, `GristBoundary`, `useGristSchema`,
|
|
78
|
+
`useWidgetMetadata`, `checkGristCell`, `encodeGristCell`, the value codec, the attachment
|
|
79
|
+
helpers, the user-action builders for `applyActions`, and Grist's types.
|
|
80
|
+
[API reference](https://gristwidgets.com/docs/sdk/api).
|
|
81
|
+
- **`grist-widget-sdk/app`**: `mountGristWidget`, `GristWidgetApp`,
|
|
82
|
+
`useTheme`, and the deploy-channel helpers the scaffold's pages use.
|
|
83
|
+
[Reference](https://gristwidgets.com/docs/sdk/api/app).
|
|
84
|
+
- **`grist-widget-sdk/emulator`**: `createGristEmulator`, document
|
|
85
|
+
`presets`, `parseDocument`. [Reference](https://gristwidgets.com/docs/emulator/api).
|
|
86
|
+
- **`grist-widget-sdk/emulator/testing`**: `renderWithGrist`,
|
|
87
|
+
`smokeTestWidget`, `installJsdomStubs`, the matchers (`actionsOf`,
|
|
88
|
+
`eventsOf`, `waitForEvent`), `presets`, and the usual
|
|
89
|
+
`@testing-library/react` helpers. [Testing](https://gristwidgets.com/docs/emulator/testing).
|
|
90
|
+
- **`grist-widget-sdk/tsconfig/app.json`**, **`/tsconfig/node.json`**: the
|
|
91
|
+
compiler settings a scaffolded widget extends.
|
|
92
|
+
|
|
93
|
+
ESM and CJS builds, with type declarations.
|
|
94
|
+
|
|
95
|
+
## Testing a widget
|
|
93
96
|
|
|
94
97
|
```tsx
|
|
95
|
-
import {
|
|
96
|
-
import {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (w.mode === "empty") return <p>Select a row.</p>
|
|
104
|
-
if (w.mode === "new-row") return <p>New row flow</p>
|
|
105
|
-
|
|
106
|
-
return <p>Record id: {String(w.record?.id ?? "")}</p>
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function App() {
|
|
110
|
-
return (
|
|
111
|
-
<GristWidgetProvider options={{ requiredAccess: "read table" }}>
|
|
112
|
-
<GristBoundary>
|
|
113
|
-
<WidgetContent />
|
|
114
|
-
</GristBoundary>
|
|
115
|
-
</GristWidgetProvider>
|
|
116
|
-
)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
createRoot(document.getElementById("root")!).render(
|
|
120
|
-
<StrictMode>
|
|
121
|
-
<App />
|
|
122
|
-
</StrictMode>
|
|
123
|
-
)
|
|
98
|
+
import { fireEvent, renderWithGrist, screen, waitFor } from "grist-widget-sdk/emulator/testing"
|
|
99
|
+
import App, { widget } from "./App"
|
|
100
|
+
|
|
101
|
+
it("marks the task done", async () => {
|
|
102
|
+
const { emulator } = renderWithGrist(<App />, { widget, rows: [{ Title: "Ship", Done: false }] })
|
|
103
|
+
fireEvent.click(await screen.findByRole("button", { name: "Ship" }))
|
|
104
|
+
await waitFor(() => expect(emulator.state.tables.get("Table1")!.rows[0]).toMatchObject({ Done: true }))
|
|
105
|
+
})
|
|
124
106
|
```
|
|
125
107
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
- `grist-widget-sdk` — production surface:
|
|
131
|
-
- Components: `GristWidgetProvider`, `GristBoundary`, `useGristContext`, `useGristOptional`
|
|
132
|
-
- Primary hook: `useGrist` (includes `capabilities` — `canRender`, `canWriteRecords`, …)
|
|
133
|
-
- Slice hooks: `useGristStatus`, `useGristSelection`, `useGristWrites`, `useGristTheme`
|
|
134
|
-
- Schema hook: `useGristSchema`
|
|
135
|
-
- Helpers: action builders, value codec, safe parsing, replica builders,
|
|
136
|
-
REST auth, attachments, column-mapping validation, alerts.
|
|
137
|
-
|
|
138
|
-
Subpath exports:
|
|
139
|
-
|
|
140
|
-
- `grist-widget-sdk/advanced` — low-level / mid-level hooks for power users
|
|
141
|
-
(`useGristReady`, `useGristAvailability`, `useGristRecord`, `useGristRecords`,
|
|
142
|
-
`useGristActions`, `useGristTableOps`, `useGristMappedRecord`,
|
|
143
|
-
`useGristRowsFromTable`, `useGristAccessToken`, `useGristSection`,
|
|
144
|
-
`useGristWidgetOptions`, `useGristTable`).
|
|
145
|
-
- `grist-widget-sdk/emulator` — `createGristEmulator`, `parseDocument`,
|
|
146
|
-
`safeParseDocument`, `presets`, emulator types.
|
|
147
|
-
|
|
148
|
-
## Replica document (`GristReplicaDocument`)
|
|
108
|
+
The emulator runs in the test process and stands in for `window.grist`. It
|
|
109
|
+
is a test harness, not a preview: to see a widget run, use a real Grist
|
|
110
|
+
(`pnpm demo` in a scaffolded widget, or its demo page).
|
|
149
111
|
|
|
150
|
-
|
|
112
|
+
## Developing this package
|
|
151
113
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
| Field | Required | Notes |
|
|
155
|
-
| --- | --- | --- |
|
|
156
|
-
| `generatedAt` | yes | ISO 8601 string |
|
|
157
|
-
| `tables` | yes | `Record<tableId, GristReplicaTable>` — keys align with `docApi.listTables()` / `fetchTable(tableId)` |
|
|
158
|
-
| `source` | no | Provenance (e.g. pass `useGristSchema({ source })` or `buildReplicaDocumentFromDocApi(docApi, { source })`); hosts may log it |
|
|
159
|
-
| `docName`, `mode`, `selection`, `widgetOptions`, `widgetInteraction`, `configuration`, `accessToken` | no | Same semantics as in types |
|
|
160
|
-
|
|
161
|
-
Optional **`mode`**: `"schema-only"` \| `"schema+samples"` \| `"schema+data"` (hint only). At boundaries, use **`normalizeReplicaTableInput`** / **`normalizeReplicaDocumentInput`** so missing **`rows`** behaves like **`[]`**.
|
|
162
|
-
|
|
163
|
-
### Tables and rows
|
|
164
|
-
|
|
165
|
-
- **`GristReplicaTable`**: optional `label`, `columns`, `rows`, `rowCount`, `dataOmitted`, etc.
|
|
166
|
-
- Row bodies are **`GristReplicaRow`** (`{ id: number, … }`), **not** columnar `fetchTable` wire format. Use **`buildReplicaDocumentFromDocApi`** or **`decodeColumnarToReplicaRows`** to build rows from `docApi.fetchTable`.
|
|
167
|
-
|
|
168
|
-
### Building and serializing
|
|
169
|
-
|
|
170
|
-
- Prefer **`buildReplicaDocumentFromDocApi(docApi, { includeRows: false \| "samples" \| "full", sampleRowLimit, source, includeSystemTableData })`**, or **`w.buildReplicaDocumentFromDocApi(...)`** from **`useGrist()`**.
|
|
171
|
-
- **`getDocumentJson` / `buildDocumentJson`** accept **`includeRows`**: `"none"` \| `"samples"` \| `"full"` (plus optional **`sampleRowLimit`**, **`space`**).
|
|
172
|
-
|
|
173
|
-
### Emulator
|
|
174
|
-
|
|
175
|
-
Only a full **`GristReplicaDocument`** is accepted: **`createGristEmulator({ document })`** and **`parseDocument`**. There is no legacy coercion or alternate “emulator input” type; assemble the object (or load JSON), then **`parseDocument`** if you need validation.
|
|
176
|
-
|
|
177
|
-
The canonical design document for this shape is [Replica document](https://gristwidgets.com/docs/sdk/design/replica-document).
|
|
178
|
-
- `grist-widget-sdk/emulator/transports` — low-level `iframe` and `inline` transports if you need to wire your own host.
|
|
179
|
-
- `grist-widget-sdk/emulator/devtools` — `<EmulatorInspector />` React component (table editor + event timeline + options).
|
|
180
|
-
- `grist-widget-sdk/emulator/testing` — `renderWithGrist(...)`, vitest
|
|
181
|
-
matchers (`actionsOf`, `eventsOf`, `waitForEvent`), document `presets`,
|
|
182
|
-
and the most-used `@testing-library/react` primitives (`screen`,
|
|
183
|
-
`fireEvent`, `waitFor`, `act`, `cleanup`, `within`, `render`) so a typical
|
|
184
|
-
test file only needs one import line.
|
|
185
|
-
|
|
186
|
-
## Emulator (SOTA)
|
|
187
|
-
|
|
188
|
-
The emulator is the single source of truth for three use-cases:
|
|
189
|
-
|
|
190
|
-
1. **Demoing widgets** — production-fidelity by loading the *real*
|
|
191
|
-
`grist-plugin-api.js` inside an iframe and speaking `grain-rpc` over
|
|
192
|
-
`postMessage`:
|
|
193
|
-
|
|
194
|
-
```tsx
|
|
195
|
-
import { createGristEmulator, presets } from "grist-widget-sdk/emulator"
|
|
196
|
-
import { EmulatorInspector } from "grist-widget-sdk/emulator/devtools"
|
|
197
|
-
|
|
198
|
-
const iframe = document.querySelector("iframe")!
|
|
199
|
-
const emulator = createGristEmulator({
|
|
200
|
-
document: presets.todoList(),
|
|
201
|
-
transport: { kind: "iframe", iframe },
|
|
202
|
-
})
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
2. **Robust SDK testing** — `renderWithGrist` synthesises `window.grist`
|
|
206
|
-
in-process, so every `useGrist` test goes through the *exact* same code
|
|
207
|
-
path without ceremony:
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
import { renderWithGrist } from "grist-widget-sdk/emulator/testing"
|
|
211
|
-
|
|
212
|
-
const { emulator, getByText } = renderWithGrist(<MyWidget />, {
|
|
213
|
-
emulator: { document: presets.contacts() },
|
|
214
|
-
})
|
|
215
|
-
emulator.mutate.addRow("Contacts", { firstName: "Grace", lastName: "Hopper" })
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
3. **Live IDE preview** — same `inline` transport, mounted next to the
|
|
219
|
-
widget to give a Storybook-grade dev loop.
|
|
220
|
-
|
|
221
|
-
## Local Development
|
|
222
|
-
|
|
223
|
-
From the workspace root:
|
|
114
|
+
From the monorepo root:
|
|
224
115
|
|
|
225
116
|
```bash
|
|
226
117
|
pnpm --filter grist-widget-sdk build
|
|
227
|
-
pnpm --filter grist-widget-sdk dev
|
|
228
118
|
pnpm --filter grist-widget-sdk test
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Pre-Commit / Pre-Publish Checks
|
|
232
|
-
|
|
233
|
-
```bash
|
|
234
|
-
pnpm --filter grist-widget-sdk build
|
|
235
119
|
pnpm --filter grist-widget-sdk exec tsc --noEmit
|
|
236
|
-
pnpm --filter grist-widget-sdk exec vitest run
|
|
237
|
-
pnpm dlx @arethetypeswrong/cli -P packages/core
|
|
238
120
|
```
|
|
239
|
-
|
|
240
|
-
If no tests exist yet:
|
|
241
|
-
|
|
242
|
-
```bash
|
|
243
|
-
pnpm --filter grist-widget-sdk exec vitest run --passWithNoTests
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
## Notes
|
|
247
|
-
|
|
248
|
-
- This package targets pure TypeScript/React usage (no Next.js dependency required).
|
|
249
|
-
- ATTW may report a `node10` warning for the `grist-widget-sdk/emulator` subpath. This is expected for modern `exports`-based packages and is non-blocking for Node 16+ and modern bundlers.
|