@notionhq/custom-blocks 0.1.43 → 0.1.44
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/README.md +2 -2
- package/dist/bridge/SandboxBridge.d.ts +9 -2
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +23 -8
- package/dist/bridge/dataSources/subscribe.d.ts +3 -0
- package/dist/bridge/dataSources/subscribe.d.ts.map +1 -0
- package/dist/bridge/dataSources/subscribe.js +63 -0
- package/dist/bridge/notifyListener.d.ts +3 -0
- package/dist/bridge/notifyListener.d.ts.map +1 -0
- package/dist/bridge/notifyListener.js +16 -0
- package/dist/bridge/sandboxClient.d.ts +3 -3
- package/dist/bridge/sandboxClient.d.ts.map +1 -1
- package/dist/bridge/sandboxClient.js +3 -3
- package/dist/customBlock.d.ts +6 -0
- package/dist/customBlock.d.ts.map +1 -1
- package/dist/customBlock.js +6 -0
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/messages/sandboxToHost.d.ts +3 -0
- package/dist/protocol/messages/sandboxToHost.js +2 -0
- package/dist/protocol/messages/unsubscribeDataSourceQuery.d.ts +10 -0
- package/dist/protocol/messages/unsubscribeDataSourceQuery.js +9 -0
- package/dist/react/useDataSource.d.ts.map +1 -1
- package/dist/react/useDataSource.js +26 -29
- package/dist/types.d.ts +15 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/docs/data-sources.md +212 -72
- package/docs/lifecycle.md +1 -11
- package/docs/pages.md +0 -2
- package/docs/users.md +0 -1
- package/package.json +1 -1
- package/src/bridge/SandboxBridge.ts +41 -13
- package/src/bridge/dataSources/subscribe.ts +97 -0
- package/src/bridge/notifyListener.ts +14 -0
- package/src/bridge/sandboxClient.ts +7 -8
- package/src/customBlock.ts +7 -0
- package/src/react/useDataSource.ts +32 -39
- package/src/types.ts +18 -15
package/docs/data-sources.md
CHANGED
|
@@ -1,47 +1,74 @@
|
|
|
1
1
|
# Data sources
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read the Notion rows your block needs with filters, sorts, and row limits. Your subscription keeps them current as the data changes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use `customBlock.subscribeToDataSource(...)` with any TypeScript UI framework. For React, `useDataSource(key, options)` provides the latest snapshot and handles subscription cleanup.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Each snapshot contains rows, property schemas, loading and error state, and `hasMore`. Each query result replaces the previous rows in the snapshot. Subscriptions remain independent, even when they query the same data source.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Queries default to 20 rows and support up to 999. See [Known limitations](#known-limitations) for pagination, filter, and sort restrictions.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Bind a data source
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Declare the data your block needs under `dataSources` in `worker.customBlock(...)`. Give each declaration a key, such as `people`, and declare the properties your block uses.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
- `propertiesById[propertyId]` — keyed by the raw Notion property ID.
|
|
15
|
+
When someone adds the block to Notion, they bind each declaration to a data source and map its properties. Your code queries the declared key and the SDK resolves the binding. This lets the same block work with different data sources across workspaces.
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
Prefer `propertiesByKey` when reading declared properties. These keys stay the same across bindings. `propertiesById` exposes values keyed by the bound data source's raw property IDs.
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
Use `ManifestDataSource`, `ManifestProperty`, and `ManifestIcon` for declaration metadata. To list declared keys, use `useManifest()` in React or `customBlock.getManifest()` in TypeScript. These APIs are useful for a data source switcher.
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
## Subscribe to rows
|
|
22
|
+
|
|
23
|
+
### `customBlock.subscribeToDataSource(args)`
|
|
23
24
|
|
|
24
25
|
```ts
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
function customBlock.subscribeToDataSource(
|
|
27
|
+
args: SubscribeToDataSourceArgs,
|
|
28
|
+
): () => void;
|
|
29
|
+
|
|
30
|
+
type SubscribeToDataSourceArgs = {
|
|
31
|
+
key: string;
|
|
32
|
+
onSnapshot: (snapshot: DataSourceSnapshot) => void;
|
|
33
|
+
options?: DataSourceQueryOptions;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type DataSourceQueryOptions = {
|
|
37
|
+
limit?: number;
|
|
38
|
+
filter?: NotionDataSourceFilter;
|
|
39
|
+
sorts?: NotionDataSourceSort[];
|
|
40
|
+
};
|
|
31
41
|
```
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
Each call registers the `onSnapshot` listener. When you unsubscribe that listener, other listeners remain subscribed. Queries start only after SDK initialization.
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
The listener receives the current snapshot immediately and receives later updates. Unrelated host changes do not trigger the listener. A new query result can trigger the listener even if its row values are unchanged. Each snapshot contains rows, schema, loading state, and row methods for updating, archiving, and unarchiving.
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
The SDK calls `onSnapshot` immediately with the current snapshot, before `subscribeToDataSource` returns.
|
|
38
48
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
```ts
|
|
50
|
+
import { customBlock, initCustomBlock } from "@notionhq/custom-blocks";
|
|
51
|
+
|
|
52
|
+
await initCustomBlock();
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
const unsubscribe = customBlock.subscribeToDataSource({
|
|
55
|
+
key: "people",
|
|
56
|
+
onSnapshot: (snapshot) => render(snapshot),
|
|
57
|
+
options: {
|
|
58
|
+
limit: 50,
|
|
59
|
+
sorts: [{ key: "name", direction: "ascending" }],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// When the renderer unmounts:
|
|
64
|
+
unsubscribe();
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To change the query or request more rows, unsubscribe and create a new subscription with different options.
|
|
68
|
+
|
|
69
|
+
Both APIs accept the [query options](#query-options) below. The SDK copies options when you subscribe. Later changes to the supplied object do not affect the subscription. A change to the data source definition repeats the query with the current options.
|
|
70
|
+
|
|
71
|
+
`unsubscribe()` removes this SDK subscription and its listener, then sends `unsubscribeDataSourceQuery` to the host. The host stops future refreshes and ignores pending results for that subscription. Other subscriptions remain active. Repeated calls have no effect.
|
|
45
72
|
|
|
46
73
|
### `useDataSource(key, options?)`
|
|
47
74
|
|
|
@@ -67,80 +94,148 @@ type UseDataSourceResult = {
|
|
|
67
94
|
};
|
|
68
95
|
```
|
|
69
96
|
|
|
70
|
-
Reads the data source mapped to `key`.
|
|
97
|
+
Reads the data source mapped to `key`. To request more rows, store the limit in component state and pass it to `useDataSource` again.
|
|
98
|
+
|
|
99
|
+
`propertyIdsByKey` maps manifest property keys to raw Notion property IDs; unbound keys map to `undefined`. `propertySchemasByKey` exposes the corresponding schemas and is likewise `undefined` for declared-but-unbound slots.
|
|
100
|
+
|
|
101
|
+
`useDataSource` uses `customBlock.subscribeToDataSource` and returns its latest snapshot. A change to the key, limit, filter, or sorts replaces the subscription.
|
|
102
|
+
|
|
103
|
+
Each consumer has its own host subscription. On unmount, the SDK removes its local query state and sends an unsubscribe message. The host stops future refreshes and ignores pending results. Other consumers remain subscribed.
|
|
104
|
+
|
|
105
|
+
Query failures follow the SDK's [error-handling contract](./errors.md).
|
|
106
|
+
|
|
107
|
+
## Query options
|
|
108
|
+
|
|
109
|
+
Both subscription APIs accept `limit`, `filter`, and `sorts`.
|
|
71
110
|
|
|
72
|
-
|
|
111
|
+
### Row limits
|
|
112
|
+
|
|
113
|
+
`limit` defaults to 20 and must be a positive integer. The SDK caps values above 999. Each query reads from the start of the matching rows. A larger limit replaces the previous snapshot with a larger result.
|
|
114
|
+
|
|
115
|
+
`hasMore` indicates that more matching rows exist beyond the result. It does not provide a cursor or guarantee that increasing the limit can retrieve every row.
|
|
116
|
+
|
|
117
|
+
### Filters
|
|
118
|
+
|
|
119
|
+
Use `filter` to select matching rows. Filters do not support every property type yet. Check [Property support](#property-support) before choosing a property.
|
|
120
|
+
|
|
121
|
+
Use a single condition to match unfinished tasks:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
filter: {
|
|
125
|
+
key: "done",
|
|
126
|
+
checkbox: { equals: false },
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Use `and` to match unfinished tasks whose titles also contain "launch":
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
filter: {
|
|
134
|
+
and: [
|
|
135
|
+
{ key: "title", title: { contains: "launch" } },
|
|
136
|
+
{ key: "done", checkbox: { equals: false } },
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Here, `title` and `done` are declared property keys. Use `propertyId` instead of `key` when you have a raw property ID. The condition type, such as `checkbox`, must match the property's schema.
|
|
142
|
+
|
|
143
|
+
Pass a single condition directly, or combine up to 25 conditions with `and`. Nested groups and `or` are unsupported. An empty `and` group applies no conditions. Both APIs accept the same filter shape.
|
|
144
|
+
|
|
145
|
+
The SDK reports invalid keys, types, operators, and values through the snapshot's `error` field.
|
|
146
|
+
|
|
147
|
+
<details>
|
|
148
|
+
<summary>Filter operators and values</summary>
|
|
149
|
+
|
|
150
|
+
| Property types | Filter operators |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `title`, `rich_text`, `url`, `email`, `phone_number` | `equals`, `does_not_equal`, `contains`, `does_not_contain`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty` |
|
|
153
|
+
| `number` | `equals`, `does_not_equal`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, `is_not_empty` |
|
|
154
|
+
| `checkbox` | `equals`, `does_not_equal` |
|
|
155
|
+
| `select`, `status` | `equals`, `does_not_equal`, `is_empty`, `is_not_empty` |
|
|
156
|
+
| `multi_select` | `contains`, `contains_all`, `does_not_contain`, `is_empty`, `is_not_empty` |
|
|
157
|
+
| `date` | `equals`, `before`, `after`, `on_or_before`, `on_or_after`, `is_empty`, `is_not_empty` |
|
|
158
|
+
|
|
159
|
+
Select, multi-select, and status values can be one option name or an array of option names. For multi-select properties, `contains` matches any supplied option and `contains_all` requires every supplied option. Empty checks use `{ is_empty: true }` or `{ is_not_empty: true }`. Date comparisons accept ISO dates or ISO timestamps.
|
|
160
|
+
|
|
161
|
+
</details>
|
|
162
|
+
|
|
163
|
+
### Sorts
|
|
164
|
+
|
|
165
|
+
Use `sorts` to order matching rows. Sorts do not support every property type yet, including some types that support filters. Check [Property support](#property-support) before choosing a property.
|
|
166
|
+
|
|
167
|
+
This example sorts tasks by due date, with the earliest first. Tasks with the same due date use their creation time, newest first:
|
|
73
168
|
|
|
74
169
|
```tsx
|
|
75
170
|
const query = useDataSource("tasks", {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
],
|
|
81
|
-
},
|
|
82
|
-
sorts: [{ key: "due", direction: "ascending" }],
|
|
171
|
+
sorts: [
|
|
172
|
+
{ key: "due", direction: "ascending" },
|
|
173
|
+
{ propertyId: "created_time", direction: "descending" },
|
|
174
|
+
],
|
|
83
175
|
limit: 50,
|
|
84
176
|
});
|
|
85
177
|
```
|
|
86
178
|
|
|
87
|
-
|
|
179
|
+
Here, `due` is a declared date property key and `created_time` is a built-in property ID. Sorts apply in array order, with the first property taking priority. Empty values sort last in both directions.
|
|
88
180
|
|
|
89
|
-
|
|
90
|
-
- `number`: `equals`, `does_not_equal`, `greater_than`, `less_than`, `greater_than_or_equal_to`, `less_than_or_equal_to`, `is_empty`, and `is_not_empty`.
|
|
91
|
-
- `checkbox`: `equals` and `does_not_equal`.
|
|
92
|
-
- `select` and `status`: `equals`, `does_not_equal`, `is_empty`, and `is_not_empty`.
|
|
93
|
-
- `multi_select`: `contains`, `contains_all`, `does_not_contain`, `is_empty`, and `is_not_empty`.
|
|
94
|
-
- `date`: `equals`, `before`, `after`, `on_or_before`, `on_or_after`, `is_empty`, and `is_not_empty`.
|
|
181
|
+
You can sort by up to 10 unique properties. Both APIs accept the same sort shape, and you can combine `sorts` with `filter`.
|
|
95
182
|
|
|
96
|
-
|
|
183
|
+
## Pages within a data source
|
|
97
184
|
|
|
98
|
-
|
|
185
|
+
Each row returned by either API is a `NotionDataSourcePage` — `{ id, propertiesById, propertiesByKey, update, archive, unarchive }`. Read property values through either of the two views:
|
|
99
186
|
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
resolves semantic keys before it sends the bridge request. The host applies
|
|
103
|
-
sorts in array order. The first sort has the highest priority. The list can
|
|
104
|
-
contain up to ten unique properties. Empty values sort last in either
|
|
105
|
-
direction. Sorts currently support title, rich text, number, checkbox, URL,
|
|
106
|
-
email, phone number, date, created time, and last edited time properties.
|
|
107
|
-
Other property types are rejected until their sort values are supported.
|
|
187
|
+
- `propertiesByKey[key]` — keyed by the semantic property keys you declared in the manifest.
|
|
188
|
+
- `propertiesById[propertyId]` — keyed by the raw Notion property ID.
|
|
108
189
|
|
|
109
|
-
|
|
190
|
+
The four built-ins (`created_time`, `last_edited_time`, `created_by`, `last_edited_by`) are always present in `propertiesById` and `collectionSchema.propertiesById`. They have no semantic keys, so they do not appear in the `*ByKey` views.
|
|
110
191
|
|
|
111
|
-
|
|
192
|
+
### Updating a row
|
|
112
193
|
|
|
113
|
-
|
|
194
|
+
Each page carries its own `update` helper:
|
|
114
195
|
|
|
115
196
|
```ts
|
|
116
|
-
|
|
197
|
+
await row.update({
|
|
198
|
+
properties: {
|
|
199
|
+
score: { type: "number", number: 8 }, // semantic key
|
|
200
|
+
},
|
|
201
|
+
icon: { type: "emoji", emoji: "✅" },
|
|
202
|
+
});
|
|
117
203
|
```
|
|
118
204
|
|
|
119
|
-
|
|
205
|
+
Property values can be keyed by **either** semantic keys or raw IDs. The SDK resolves semantic keys to IDs before sending the request.
|
|
120
206
|
|
|
121
|
-
|
|
122
|
-
const query = useDataSource("tasks");
|
|
123
|
-
```
|
|
207
|
+
Use `row.update(...)` whenever you already have a row in hand. For pages you don't have a row for (e.g. you only have a `pageId`), drop down to the top-level [`pages` API](./pages.md) — it covers create / get / update / archive / unarchive and accepts raw property IDs only.
|
|
124
208
|
|
|
125
|
-
|
|
209
|
+
### Archiving and unarchiving a row
|
|
126
210
|
|
|
127
|
-
|
|
211
|
+
`row.archive()` is shorthand for calling `row.update({ is_archived: true })`.
|
|
212
|
+
`row.unarchive()` is shorthand for calling `row.update({ is_archived: false })`.
|
|
213
|
+
Both methods return an `UpdatePageResult`.
|
|
214
|
+
Use `row.update` to change archive status and other fields in one request.
|
|
128
215
|
|
|
129
|
-
|
|
216
|
+
## Read declarations
|
|
217
|
+
|
|
218
|
+
Use the manifest when your UI needs to discover which data sources and properties the block declares. For example, build a data source switcher from the declared keys or table headings from the declared property names.
|
|
219
|
+
|
|
220
|
+
Both APIs below return the declaration from `worker.customBlock(...)`, including its keys, names, descriptions, and property definitions. To query a selected data source, pass its key to either subscription API. Read resolved property IDs from the query snapshot's `propertyIdsByKey` map.
|
|
221
|
+
|
|
222
|
+
Both APIs return synchronously and throw if called before `initCustomBlock()` resolves.
|
|
223
|
+
|
|
224
|
+
### `useManifest()`
|
|
130
225
|
|
|
131
226
|
```ts
|
|
132
|
-
function
|
|
227
|
+
function useManifest(): CustomBlockManifest;
|
|
133
228
|
```
|
|
134
229
|
|
|
135
|
-
|
|
230
|
+
Returns the block manifest received from the host during initialization. The manifest contains declarations only. Resolved bindings and rows are available through query snapshots.
|
|
136
231
|
|
|
137
|
-
|
|
138
|
-
await initCustomBlock();
|
|
232
|
+
### `customBlock.getManifest()`
|
|
139
233
|
|
|
140
|
-
|
|
234
|
+
```ts
|
|
235
|
+
function customBlock.getManifest(): CustomBlockManifest;
|
|
141
236
|
```
|
|
142
237
|
|
|
143
|
-
|
|
238
|
+
Framework-neutral getter for the same manifest returned by `useManifest()`. The manifest is static for the lifetime of the sandbox, so there is nothing to subscribe to.
|
|
144
239
|
|
|
145
240
|
## Example: querying a data source
|
|
146
241
|
|
|
@@ -185,10 +280,10 @@ export function ScoreList() {
|
|
|
185
280
|
</li>
|
|
186
281
|
))}
|
|
187
282
|
</ul>
|
|
188
|
-
{hasMore ? (
|
|
283
|
+
{hasMore && limit < 999 ? (
|
|
189
284
|
<button
|
|
190
285
|
type="button"
|
|
191
|
-
onClick={() => setLimit(limit + 20)}
|
|
286
|
+
onClick={() => setLimit(Math.min(limit + 20, 999))}
|
|
192
287
|
disabled={isLoading}
|
|
193
288
|
>
|
|
194
289
|
{isLoading ? "Loading…" : "Load more"}
|
|
@@ -199,6 +294,50 @@ export function ScoreList() {
|
|
|
199
294
|
}
|
|
200
295
|
```
|
|
201
296
|
|
|
297
|
+
## Known limitations
|
|
298
|
+
|
|
299
|
+
- **Row limits and pagination:** `limit` defaults to 20 and is capped at 999. Queries have no cursor or offset. Increasing the limit repeats the query from the start and replaces the snapshot. `hasMore` can remain true at the cap, so use filters to narrow larger data sources.
|
|
300
|
+
- **Filter and sort rules:** Use one property condition or one `and` group with up to 25 conditions. Nested groups and `or` are unsupported. Sort by up to 10 unique properties. Empty values sort last in both directions. See the table below for property support and [Filters](#filters) for operators.
|
|
301
|
+
- **Changing a query:** TypeScript subscriptions copy their options when they start. To change them, unsubscribe and create a new subscription. React replaces the subscription when the key or options change. Previous rows are cleared while the new query loads.
|
|
302
|
+
- **Updates:** Callbacks receive complete snapshots, not individual row changes. A new result can trigger a callback even when its row values are unchanged.
|
|
303
|
+
|
|
304
|
+
### Property support
|
|
305
|
+
|
|
306
|
+
The table shows the values returned by the Notion host and whether each property type supports filtering and sorting. ✅ means supported. ❌ means unsupported. ⚠️ marks a text fallback that does not preserve the property's structured value. Missing values can be `undefined`.
|
|
307
|
+
|
|
308
|
+
| Property type | Query result | Filter | Sort |
|
|
309
|
+
| --- | --- | :---: | :---: |
|
|
310
|
+
| `title` | Plain text | ✅ | ✅ |
|
|
311
|
+
| `rich_text` | Plain text | ✅ | ✅ |
|
|
312
|
+
| `number` | Number | ✅ | ✅ |
|
|
313
|
+
| `checkbox` | Boolean | ✅ | ✅ |
|
|
314
|
+
| `url` | String | ✅ | ✅ |
|
|
315
|
+
| `email` | String | ✅ | ✅ |
|
|
316
|
+
| `phone_number` | String | ✅ | ✅ |
|
|
317
|
+
| `select` | Option name | ✅ | ❌ |
|
|
318
|
+
| `multi_select` | Array of option names | ✅ | ❌ |
|
|
319
|
+
| `status` | Option name | ✅ | ❌ |
|
|
320
|
+
| `date` | `NotionDateValue` | ✅ | ✅ |
|
|
321
|
+
| `people` | Array of record pointers | ❌ | ❌ |
|
|
322
|
+
| `files` | ⚠️ Text fallback | ❌ | ❌ |
|
|
323
|
+
| `unique_id` | ⚠️ Text fallback | ❌ | ❌ |
|
|
324
|
+
| `relation` | Array of record pointers | ❌ | ❌ |
|
|
325
|
+
| `place` | ⚠️ Text fallback | ❌ | ❌ |
|
|
326
|
+
| `formula` | ⚠️ Text fallback | ❌ | ❌ |
|
|
327
|
+
| `rollup` | ⚠️ Text fallback | ❌ | ❌ |
|
|
328
|
+
| `button` | ⚠️ Text fallback | ❌ | ❌ |
|
|
329
|
+
| `verification` | ⚠️ Text fallback | ❌ | ❌ |
|
|
330
|
+
| `last_visited_time` | ⚠️ Text fallback | ❌ | ❌ |
|
|
331
|
+
| `location` | ⚠️ Text fallback | ❌ | ❌ |
|
|
332
|
+
| `created_time` | `NotionDateTime` in UTC | ❌ | ✅ |
|
|
333
|
+
| `last_edited_time` | `NotionDateTime` in UTC | ❌ | ✅ |
|
|
334
|
+
| `created_by` | Array of record pointers | ❌ | ❌ |
|
|
335
|
+
| `last_edited_by` | Array of record pointers | ❌ | ❌ |
|
|
336
|
+
|
|
337
|
+
Text values omit formatting, mention tokens, and annotations. Record pointers contain a table name and a record ID.
|
|
338
|
+
|
|
339
|
+
Use `propertyId: "created_time"` or `propertyId: "last_edited_time"` to sort by these built-in timestamps.
|
|
340
|
+
|
|
202
341
|
## Types
|
|
203
342
|
|
|
204
343
|
### Rows & values
|
|
@@ -207,14 +346,15 @@ export function ScoreList() {
|
|
|
207
346
|
- `NotionDataSourcePage` — a single row exposed to app code: `{ id, propertiesById, propertiesByKey, update, archive, unarchive }`.
|
|
208
347
|
- `NotionDataSourceValue` — the value union for `propertiesById` and `propertiesByKey`.
|
|
209
348
|
- `NotionDataSourcePageUpdateArgs` / `UpdatePageResult` — arguments and result for the per-page `update` helper.
|
|
210
|
-
- `NotionDataSourcePageUpdateInput` — deprecated alias for `NotionDataSourcePageUpdateArgs`.
|
|
211
|
-
- `NotionDataSourcePageUpdateResult` — deprecated alias for `UpdatePageResult`.
|
|
212
349
|
- `UseDataSourceOptions` — options accepted by `useDataSource`: `limit?: number`, `filter?: NotionDataSourceFilter`, and `sorts?: NotionDataSourceSort[]`.
|
|
213
350
|
- `NotionDataSourceFilter` — one property condition or one shallow `and` group.
|
|
214
351
|
- `NotionDataSourcePropertyFilter` — one property address combined with a type-specific operator.
|
|
215
352
|
- `NotionDataSourcePropertyAddress` — either a semantic property `key` or a raw `propertyId`.
|
|
216
353
|
- `NotionDataSourceSort` — a property address with an `ascending` or `descending` direction.
|
|
217
354
|
- `NotionDataSourceTextFilterOperator`, `NotionDataSourceNumberFilterOperator`, `NotionDataSourceCheckboxFilterOperator`, `NotionDataSourceOptionFilterOperator`, `NotionDataSourceContainsFilterOperator`, and `NotionDataSourceDateFilterOperator` — the operators accepted for each property type.
|
|
355
|
+
- `DataSourceSnapshot` — current rows, schema, and status for one live data-source query.
|
|
356
|
+
- `DataSourceQueryOptions` — the row limit, filter, and sorts shared by both query APIs.
|
|
357
|
+
- `SubscribeToDataSourceArgs` — the key, `onSnapshot` listener, and optional query options for `customBlock.subscribeToDataSource`.
|
|
218
358
|
|
|
219
359
|
### Property schemas
|
|
220
360
|
|
package/docs/lifecycle.md
CHANGED
|
@@ -150,7 +150,7 @@ All code types are open string unions, so keep a default branch. Use `isRetryabl
|
|
|
150
150
|
|
|
151
151
|
Framework-neutral runtime APIs for renderers that do not use React hooks. `customBlock.getState()` returns a `CustomBlockState` snapshot that includes `theme` and `contrastMode` while hiding internal query cache details. All getters that read host state — `getTheme`, `getContrastMode`, `getBlockId`, `getParent`, `getPage`, `getCurrentUser`, and `getManifest` — throw until `initCustomBlock()` resolves. `getManifest()` then returns the authoritative manifest supplied by the host.
|
|
152
152
|
|
|
153
|
-
`customBlock` covers runtime state
|
|
153
|
+
`customBlock` covers runtime state, live data source snapshots, and sizing. Imperative APIs such as `pages.*` and `users.*` are also framework-neutral functions.
|
|
154
154
|
|
|
155
155
|
```ts
|
|
156
156
|
await initCustomBlock();
|
|
@@ -177,16 +177,6 @@ stopAutoResize();
|
|
|
177
177
|
|
|
178
178
|
Thrown when `initCustomBlock` is called in a top-level tab (no parent frame). It extends `CustomBlockInitializationError`, has `code: "not_in_iframe"` and `isRetryable: false`, and can be detected specifically with `instanceof NotInIframeError`. `<NotionCustomBlock>` catches it and falls back to a standalone preview with a warning banner.
|
|
179
179
|
|
|
180
|
-
### `useCustomBlockAutoResize({ enabled? })`
|
|
181
|
-
|
|
182
|
-
Deprecated. Blocks resize automatically. Use CSS `max-height` and `overflow` to set a shorter height.
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
function useCustomBlockAutoResize(args?: { enabled?: boolean }): void;
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Deprecated React wrapper around `customBlock.autoResize({ target: document.getElementById("root") })`. It measures `#root`'s height and posts `resize` messages, deduping unchanged values. `<NotionCustomBlock>` runs this automatically.
|
|
189
|
-
|
|
190
180
|
## Debug console
|
|
191
181
|
|
|
192
182
|
Press `\` while focused in a custom block to toggle a debug overlay that replaces the block's children with a `<pre>` log of every `postMessage` sent and received over the bridge. Each line is formatted as:
|
package/docs/pages.md
CHANGED
|
@@ -161,7 +161,5 @@ Do **not** send `{ type: "file_upload", file_upload: { id } }`; the host will re
|
|
|
161
161
|
- `NotionPagePropertyWriteMap` — input map for `pages.update` (raw property IDs only).
|
|
162
162
|
- `NotionCreatePagePosition` — `start` / `end` / `before` / `after` insertion variants.
|
|
163
163
|
- `CreatePageArgs` / `CreatePageParent` / `CreatePageResult`.
|
|
164
|
-
- `CreatePageInput` — deprecated alias for `CreatePageArgs`.
|
|
165
164
|
- `GetPageResult`.
|
|
166
165
|
- `UpdatePageArgs` / `UpdatePageResult`.
|
|
167
|
-
- `UpdatePageInput` — deprecated alias for `UpdatePageArgs`.
|
package/docs/users.md
CHANGED
|
@@ -78,5 +78,4 @@ if (result.status === "success") {
|
|
|
78
78
|
- `useCurrentUser()` — hook that returns the viewer's `NotionUser`.
|
|
79
79
|
- `customBlock.getCurrentUser()` — framework-neutral getter for the viewer's `NotionUser`.
|
|
80
80
|
- `ListUsersArgs` / `ListUsersResult`.
|
|
81
|
-
- `ListUsersInput` — deprecated alias for `ListUsersArgs`.
|
|
82
81
|
- `GetUserResult`.
|
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ import type { ListUsersMessage } from "@notionhq/custom-blocks-protocol/messages
|
|
|
32
32
|
import type { QueryDataSourceMessage } from "@notionhq/custom-blocks-protocol/messages/queryDataSource.js"
|
|
33
33
|
import type { CustomBlockQueryDataSourceErrorInfo } from "@notionhq/custom-blocks-protocol/messages/queryDataSourceResult.js"
|
|
34
34
|
import type { ResizeMessage } from "@notionhq/custom-blocks-protocol/messages/resize.js"
|
|
35
|
+
import type { UnsubscribeDataSourceQueryMessage } from "@notionhq/custom-blocks-protocol/messages/unsubscribeDataSourceQuery.js"
|
|
35
36
|
import type { UpdatePageMessage } from "@notionhq/custom-blocks-protocol/messages/updatePage.js"
|
|
36
37
|
import { CUSTOM_BLOCK_BRIDGE_PROTOCOL_VERSION } from "@notionhq/custom-blocks-protocol/protocolVersion.js"
|
|
37
38
|
import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
|
|
@@ -40,6 +41,7 @@ import { globalPrimedResponsesCache } from "../testing.js"
|
|
|
40
41
|
import type {
|
|
41
42
|
CreatePageArgs,
|
|
42
43
|
CreatePageResult,
|
|
44
|
+
DataSourceQueryOptions,
|
|
43
45
|
GetPageResult,
|
|
44
46
|
GetUserResult,
|
|
45
47
|
ListUsersArgs,
|
|
@@ -48,7 +50,6 @@ import type {
|
|
|
48
50
|
NotionUserId,
|
|
49
51
|
UpdatePageArgs,
|
|
50
52
|
UpdatePageResult,
|
|
51
|
-
UseDataSourceOptions,
|
|
52
53
|
} from "../types.js"
|
|
53
54
|
import { unreachable } from "../utils.js"
|
|
54
55
|
import { CUSTOM_BLOCKS_SDK_VERSION } from "../version.js"
|
|
@@ -65,6 +66,7 @@ import {
|
|
|
65
66
|
type DataSourceQueryState,
|
|
66
67
|
} from "./hostState.js"
|
|
67
68
|
import type { ManifestLoadResult } from "./loadManifest.js"
|
|
69
|
+
import { notifyListener } from "./notifyListener.js"
|
|
68
70
|
import { PendingRequests } from "./pendingRequests.js"
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -88,6 +90,14 @@ const RESPONSE_TYPE_BY_REQUEST = new Map([
|
|
|
88
90
|
|
|
89
91
|
const INIT_RESULT_TIMER_FALLBACK_MS = 100
|
|
90
92
|
|
|
93
|
+
export type QueryDataSourceArgs = {
|
|
94
|
+
subscriptionId: string
|
|
95
|
+
key: string
|
|
96
|
+
options?: DataSourceQueryOptions
|
|
97
|
+
/** Send the query even if an identical query is already loading. */
|
|
98
|
+
forceRequery?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
export class SandboxBridge {
|
|
92
102
|
private hostState: CustomBlockHostState = {
|
|
93
103
|
status: "uninitialized",
|
|
@@ -281,7 +291,7 @@ export class SandboxBridge {
|
|
|
281
291
|
|
|
282
292
|
private notify = () => {
|
|
283
293
|
for (const listener of this.listeners) {
|
|
284
|
-
listener
|
|
294
|
+
notifyListener(listener)
|
|
285
295
|
}
|
|
286
296
|
}
|
|
287
297
|
|
|
@@ -437,15 +447,27 @@ export class SandboxBridge {
|
|
|
437
447
|
nextBindings,
|
|
438
448
|
})
|
|
439
449
|
this.latestDataSourceBindings = nextBindings
|
|
440
|
-
//
|
|
441
|
-
const
|
|
450
|
+
// Preserve rows only while the key still refers to the same backing data source.
|
|
451
|
+
const previousSourcesByKey = new Map(
|
|
452
|
+
hostState.dataSources.map(source => [source.key, source]),
|
|
453
|
+
)
|
|
454
|
+
const nextSourcesByKey = new Map(
|
|
455
|
+
dataSources.map(source => [source.key, source]),
|
|
456
|
+
)
|
|
442
457
|
const prunedState: Record<string, DataSourceQueryState> = {}
|
|
443
458
|
for (const [subscriptionId, state] of Object.entries(
|
|
444
459
|
hostState.dataSourceState,
|
|
445
460
|
)) {
|
|
446
|
-
|
|
447
|
-
|
|
461
|
+
const nextSource = nextSourcesByKey.get(state.dataSourceKey)
|
|
462
|
+
if (nextSource === undefined) {
|
|
463
|
+
continue
|
|
448
464
|
}
|
|
465
|
+
const previousSource = previousSourcesByKey.get(state.dataSourceKey)
|
|
466
|
+
prunedState[subscriptionId] =
|
|
467
|
+
previousSource?.collectionPointer?.id ===
|
|
468
|
+
nextSource.collectionPointer?.id
|
|
469
|
+
? state
|
|
470
|
+
: createEmptyDataSourceQueryState(state.dataSourceKey)
|
|
449
471
|
}
|
|
450
472
|
this.hostState = {
|
|
451
473
|
...hostState,
|
|
@@ -720,11 +742,12 @@ export class SandboxBridge {
|
|
|
720
742
|
return `data-source:${globalThis.crypto.randomUUID()}`
|
|
721
743
|
}
|
|
722
744
|
|
|
723
|
-
queryDataSource(
|
|
724
|
-
subscriptionId
|
|
725
|
-
key
|
|
726
|
-
options
|
|
727
|
-
|
|
745
|
+
queryDataSource({
|
|
746
|
+
subscriptionId,
|
|
747
|
+
key,
|
|
748
|
+
options = {},
|
|
749
|
+
forceRequery = false,
|
|
750
|
+
}: QueryDataSourceArgs) {
|
|
728
751
|
if (this.hostState.status !== "initialized") {
|
|
729
752
|
return
|
|
730
753
|
}
|
|
@@ -770,6 +793,7 @@ export class SandboxBridge {
|
|
|
770
793
|
const query = resolvedQuery.query
|
|
771
794
|
|
|
772
795
|
if (
|
|
796
|
+
!forceRequery &&
|
|
773
797
|
currentState.isLoading &&
|
|
774
798
|
currentState.latestQueryIdentity === query.identity
|
|
775
799
|
) {
|
|
@@ -790,8 +814,6 @@ export class SandboxBridge {
|
|
|
790
814
|
},
|
|
791
815
|
},
|
|
792
816
|
}
|
|
793
|
-
this.notify()
|
|
794
|
-
|
|
795
817
|
const outbound: QueryDataSourceMessage = {
|
|
796
818
|
type: "queryDataSource",
|
|
797
819
|
subscriptionId,
|
|
@@ -801,6 +823,7 @@ export class SandboxBridge {
|
|
|
801
823
|
...(query.sorts !== undefined ? { sorts: query.sorts } : {}),
|
|
802
824
|
}
|
|
803
825
|
this.postToHost(outbound)
|
|
826
|
+
this.notify()
|
|
804
827
|
}
|
|
805
828
|
|
|
806
829
|
private setDataSourceQueryError(
|
|
@@ -840,6 +863,11 @@ export class SandboxBridge {
|
|
|
840
863
|
...this.hostState,
|
|
841
864
|
dataSourceState,
|
|
842
865
|
}
|
|
866
|
+
const outbound: UnsubscribeDataSourceQueryMessage = {
|
|
867
|
+
type: "unsubscribeDataSourceQuery",
|
|
868
|
+
subscriptionId,
|
|
869
|
+
}
|
|
870
|
+
this.postToHost(outbound)
|
|
843
871
|
}
|
|
844
872
|
|
|
845
873
|
postResize(height: number) {
|