@notionhq/custom-blocks 0.1.42 → 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 +3 -3
- package/dist/bridge/SandboxBridge.d.ts +9 -2
- package/dist/bridge/SandboxBridge.d.ts.map +1 -1
- package/dist/bridge/SandboxBridge.js +31 -13
- 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/hostState.d.ts.map +1 -1
- package/dist/bridge/hostState.js +8 -5
- 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 +13 -4
- package/dist/bridge/sandboxClient.d.ts.map +1 -1
- package/dist/bridge/sandboxClient.js +23 -5
- 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 +29 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/docs/data-sources.md +213 -66
- package/docs/lifecycle.md +1 -11
- package/docs/pages.md +16 -8
- package/docs/users.md +0 -1
- package/package.json +1 -1
- package/src/bridge/SandboxBridge.ts +49 -19
- package/src/bridge/dataSources/subscribe.ts +97 -0
- package/src/bridge/hostState.ts +9 -6
- package/src/bridge/notifyListener.ts +14 -0
- package/src/bridge/sandboxClient.ts +31 -10
- package/src/customBlock.ts +7 -0
- package/src/react/useDataSource.ts +32 -39
- package/src/types.ts +37 -17
|
@@ -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) {
|
|
@@ -923,20 +951,22 @@ export class SandboxBridge {
|
|
|
923
951
|
}
|
|
924
952
|
|
|
925
953
|
updatePage(args: UpdatePageArgs): Promise<UpdatePageResult> {
|
|
954
|
+
// Prefer is_archived over the deprecated archived argument.
|
|
955
|
+
// TODO(custom-blocks): Remove the archived fallback when upgrading to bridge protocol v4.
|
|
956
|
+
const isArchived = args.is_archived ?? args.archived
|
|
926
957
|
return new Promise(resolve => {
|
|
927
958
|
if (
|
|
928
959
|
(args.properties === undefined ||
|
|
929
960
|
Object.keys(args.properties).length === 0) &&
|
|
930
961
|
args.icon === undefined &&
|
|
931
962
|
args.cover === undefined &&
|
|
932
|
-
|
|
963
|
+
isArchived === undefined
|
|
933
964
|
) {
|
|
934
965
|
resolve({
|
|
935
966
|
status: "error",
|
|
936
967
|
error: {
|
|
937
968
|
code: "invalid_page_update",
|
|
938
|
-
message:
|
|
939
|
-
"updatePage requires at least one of: properties, icon, cover, archived.",
|
|
969
|
+
message: "updatePage requires at least one property.",
|
|
940
970
|
isRetryable: false,
|
|
941
971
|
},
|
|
942
972
|
})
|
|
@@ -958,8 +988,8 @@ export class SandboxBridge {
|
|
|
958
988
|
if (args.cover !== undefined) {
|
|
959
989
|
outbound.cover = args.cover
|
|
960
990
|
}
|
|
961
|
-
if (
|
|
962
|
-
outbound.archived =
|
|
991
|
+
if (isArchived !== undefined) {
|
|
992
|
+
outbound.archived = isArchived
|
|
963
993
|
}
|
|
964
994
|
this.postToHost(outbound)
|
|
965
995
|
})
|
|
@@ -993,7 +1023,7 @@ export class SandboxBridge {
|
|
|
993
1023
|
properties: resolvedProperties?.properties,
|
|
994
1024
|
icon: pageUpdateArgs.icon,
|
|
995
1025
|
cover: pageUpdateArgs.cover,
|
|
996
|
-
|
|
1026
|
+
is_archived: pageUpdateArgs.is_archived ?? pageUpdateArgs.archived,
|
|
997
1027
|
})
|
|
998
1028
|
}
|
|
999
1029
|
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SubscribeToDataSourceArgs } from "../../types.js"
|
|
2
|
+
import type {
|
|
3
|
+
CustomBlockHostState,
|
|
4
|
+
InitializedHostState,
|
|
5
|
+
} from "../hostState.js"
|
|
6
|
+
import { notifyListener } from "../notifyListener.js"
|
|
7
|
+
import { customBlockDataSources, customBlockHost } from "../sandboxClient.js"
|
|
8
|
+
|
|
9
|
+
export function subscribeToDataSource({
|
|
10
|
+
key,
|
|
11
|
+
onSnapshot,
|
|
12
|
+
options: providedOptions = {},
|
|
13
|
+
}: SubscribeToDataSourceArgs): () => void {
|
|
14
|
+
const options = structuredClone(providedOptions)
|
|
15
|
+
const subscriptionId = customBlockDataSources.createSubscriptionId()
|
|
16
|
+
let active = true
|
|
17
|
+
let lastSnapshotInputs:
|
|
18
|
+
| {
|
|
19
|
+
status: CustomBlockHostState["status"]
|
|
20
|
+
dataSource: InitializedHostState["dataSources"][number] | undefined
|
|
21
|
+
queryState: InitializedHostState["dataSourceState"][string] | undefined
|
|
22
|
+
}
|
|
23
|
+
| undefined
|
|
24
|
+
let lastMatchingSignature: string | null | undefined
|
|
25
|
+
|
|
26
|
+
const deliverSnapshotIfChanged = (
|
|
27
|
+
hostState: CustomBlockHostState,
|
|
28
|
+
dataSource: InitializedHostState["dataSources"][number] | undefined,
|
|
29
|
+
queryState: InitializedHostState["dataSourceState"][string] | undefined,
|
|
30
|
+
) => {
|
|
31
|
+
if (
|
|
32
|
+
lastSnapshotInputs !== undefined &&
|
|
33
|
+
lastSnapshotInputs.status === hostState.status &&
|
|
34
|
+
lastSnapshotInputs.dataSource === dataSource &&
|
|
35
|
+
lastSnapshotInputs.queryState === queryState
|
|
36
|
+
) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const snapshot = customBlockDataSources.getView(
|
|
40
|
+
hostState,
|
|
41
|
+
key,
|
|
42
|
+
subscriptionId,
|
|
43
|
+
)
|
|
44
|
+
// Record inputs before calling consumer code, which can trigger another notification.
|
|
45
|
+
lastSnapshotInputs = { status: hostState.status, dataSource, queryState }
|
|
46
|
+
notifyListener(() => onSnapshot(snapshot))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleHostChange = () => {
|
|
50
|
+
if (!active) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
const hostState = customBlockHost.getState()
|
|
54
|
+
if (hostState.status !== "initialized") {
|
|
55
|
+
deliverSnapshotIfChanged(hostState, undefined, undefined)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const dataSource = hostState.dataSources.find(source => source.key === key)
|
|
60
|
+
const signature =
|
|
61
|
+
dataSource === undefined ? null : JSON.stringify(dataSource)
|
|
62
|
+
if (signature !== lastMatchingSignature) {
|
|
63
|
+
const forceRequery = lastMatchingSignature !== undefined
|
|
64
|
+
lastMatchingSignature = signature
|
|
65
|
+
// Querying updates SDK state and calls this listener again.
|
|
66
|
+
customBlockDataSources.query({
|
|
67
|
+
subscriptionId,
|
|
68
|
+
key,
|
|
69
|
+
options,
|
|
70
|
+
forceRequery,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
deliverSnapshotIfChanged(
|
|
75
|
+
hostState,
|
|
76
|
+
dataSource,
|
|
77
|
+
hostState.dataSourceState[subscriptionId],
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const unsubscribeHost = customBlockHost.subscribe(handleHostChange)
|
|
82
|
+
try {
|
|
83
|
+
handleHostChange()
|
|
84
|
+
} catch (error) {
|
|
85
|
+
unsubscribeHost()
|
|
86
|
+
customBlockDataSources.release(subscriptionId)
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
return () => {
|
|
90
|
+
if (!active) {
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
active = false
|
|
94
|
+
unsubscribeHost()
|
|
95
|
+
customBlockDataSources.release(subscriptionId)
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/bridge/hostState.ts
CHANGED
|
@@ -139,18 +139,21 @@ export function getDataSourceQueryView(
|
|
|
139
139
|
propertiesByKey[key] =
|
|
140
140
|
propertyId === undefined ? undefined : entry.propertiesById[propertyId]
|
|
141
141
|
}
|
|
142
|
+
const update = (args: NotionDataSourcePageUpdateArgs) =>
|
|
143
|
+
updateDataSourcePage({
|
|
144
|
+
dataSource,
|
|
145
|
+
pageId: entry.id,
|
|
146
|
+
pageUpdateArgs: args,
|
|
147
|
+
})
|
|
142
148
|
return {
|
|
143
149
|
id: entry.id,
|
|
144
150
|
is_archived: entry.is_archived,
|
|
145
151
|
in_trash: entry.in_trash,
|
|
146
152
|
propertiesById: entry.propertiesById,
|
|
147
153
|
propertiesByKey,
|
|
148
|
-
update
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
pageId: entry.id,
|
|
152
|
-
pageUpdateArgs: args,
|
|
153
|
-
}),
|
|
154
|
+
update,
|
|
155
|
+
archive: () => update({ is_archived: true }),
|
|
156
|
+
unarchive: () => update({ is_archived: false }),
|
|
154
157
|
}
|
|
155
158
|
})
|
|
156
159
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Report consumer errors without interrupting bridge operations or other listeners. */
|
|
2
|
+
export function notifyListener(listener: () => void): void {
|
|
3
|
+
try {
|
|
4
|
+
listener()
|
|
5
|
+
} catch (error) {
|
|
6
|
+
if (typeof globalThis.reportError === "function") {
|
|
7
|
+
globalThis.reportError(error)
|
|
8
|
+
} else {
|
|
9
|
+
setTimeout(() => {
|
|
10
|
+
throw error
|
|
11
|
+
}, 0)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -10,7 +10,6 @@ import type {
|
|
|
10
10
|
NotionUserId,
|
|
11
11
|
UpdatePageArgs,
|
|
12
12
|
UpdatePageResult,
|
|
13
|
-
UseDataSourceOptions,
|
|
14
13
|
} from "../types.js"
|
|
15
14
|
import {
|
|
16
15
|
type CustomBlockHostState,
|
|
@@ -18,9 +17,14 @@ import {
|
|
|
18
17
|
getDataSourceQueryView as getDataSourceQueryViewWithBridge,
|
|
19
18
|
} from "./hostState.js"
|
|
20
19
|
import type { ManifestLoadResult } from "./loadManifest.js"
|
|
21
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type MessageLogEntry,
|
|
22
|
+
type QueryDataSourceArgs,
|
|
23
|
+
SandboxBridge,
|
|
24
|
+
} from "./SandboxBridge.js"
|
|
22
25
|
|
|
23
26
|
let bridge: SandboxBridge | undefined
|
|
27
|
+
let didWarnAboutPagesDelete = false
|
|
24
28
|
|
|
25
29
|
function getBridge(): SandboxBridge {
|
|
26
30
|
if (!bridge) {
|
|
@@ -72,12 +76,8 @@ export const customBlockDataSources = {
|
|
|
72
76
|
return getBridge().createDataSourceSubscriptionId()
|
|
73
77
|
},
|
|
74
78
|
|
|
75
|
-
query: (
|
|
76
|
-
|
|
77
|
-
key: string,
|
|
78
|
-
options?: UseDataSourceOptions,
|
|
79
|
-
) => {
|
|
80
|
-
getBridge().queryDataSource(subscriptionId, key, options)
|
|
79
|
+
query: (args: QueryDataSourceArgs) => {
|
|
80
|
+
getBridge().queryDataSource(args)
|
|
81
81
|
},
|
|
82
82
|
|
|
83
83
|
release: (subscriptionId: string) => {
|
|
@@ -134,10 +134,31 @@ export const pages = {
|
|
|
134
134
|
},
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
*
|
|
137
|
+
* Archives a page.
|
|
138
|
+
*/
|
|
139
|
+
archive: (pageId: NotionPageId): Promise<UpdatePageResult> => {
|
|
140
|
+
return getBridge().updatePage({ pageId, is_archived: true })
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Unarchives a page.
|
|
145
|
+
*/
|
|
146
|
+
unarchive: (pageId: NotionPageId): Promise<UpdatePageResult> => {
|
|
147
|
+
return getBridge().updatePage({ pageId, is_archived: false })
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @deprecated This method archives the page. It does not move the page to Trash.
|
|
152
|
+
* Use `pages.archive(pageId)` instead.
|
|
138
153
|
*/
|
|
139
154
|
delete: (pageId: NotionPageId): Promise<UpdatePageResult> => {
|
|
140
|
-
|
|
155
|
+
if (!didWarnAboutPagesDelete) {
|
|
156
|
+
didWarnAboutPagesDelete = true
|
|
157
|
+
console.warn(
|
|
158
|
+
"[Notion Custom Blocks] DEPRECATED: pages.delete() archives the page. It does not move the page to Trash. Use pages.archive() instead.",
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
return getBridge().updatePage({ pageId, is_archived: true })
|
|
141
162
|
},
|
|
142
163
|
}
|
|
143
164
|
|
package/src/customBlock.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { NotionParent } from "@notionhq/custom-blocks-protocol/parent.js"
|
|
|
7
7
|
import type { NotionTheme } from "@notionhq/custom-blocks-protocol/theme.js"
|
|
8
8
|
import type { NotionUser } from "@notionhq/custom-blocks-protocol/users/user.js"
|
|
9
9
|
import { autoResize } from "./autoResize.js"
|
|
10
|
+
import { subscribeToDataSource } from "./bridge/dataSources/subscribe.js"
|
|
10
11
|
import type {
|
|
11
12
|
CustomBlockHostState,
|
|
12
13
|
InitializedHostState,
|
|
@@ -72,6 +73,12 @@ export const customBlock = {
|
|
|
72
73
|
},
|
|
73
74
|
|
|
74
75
|
autoResize,
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Registers a listener that receives the current query snapshot and later updates.
|
|
79
|
+
* Returns a function that removes this listener. Other listeners remain subscribed.
|
|
80
|
+
*/
|
|
81
|
+
subscribeToDataSource,
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
let lastHostState: CustomBlockHostState | undefined
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { useEffect, useState } from "react"
|
|
2
2
|
import { getDataSourceQueryOptionsIdentity } from "../bridge/dataSources/query.js"
|
|
3
|
-
import {
|
|
3
|
+
import { customBlock } from "../customBlock.js"
|
|
4
4
|
import type { UseDataSourceOptions, UseDataSourceResult } from "../types.js"
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
const EMPTY_DATA_SOURCE_RESULT: UseDataSourceResult = {
|
|
7
|
+
items: [],
|
|
8
|
+
propertySchemasById: {},
|
|
9
|
+
propertyIdsByKey: {},
|
|
10
|
+
propertySchemasByKey: {},
|
|
11
|
+
isLoading: false,
|
|
12
|
+
hasMore: false,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type DataSourceHookState = {
|
|
16
|
+
key: string
|
|
17
|
+
optionsIdentity: string
|
|
18
|
+
snapshot: UseDataSourceResult
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
/**
|
|
8
22
|
* Reads from the data source mapped to the given semantic `key`.
|
|
@@ -18,45 +32,24 @@ export function useDataSource(
|
|
|
18
32
|
key: string,
|
|
19
33
|
options?: UseDataSourceOptions,
|
|
20
34
|
): UseDataSourceResult {
|
|
21
|
-
const host = useCustomBlockHost()
|
|
22
|
-
const [subscriptionId] = useState(() =>
|
|
23
|
-
customBlockDataSources.createSubscriptionId(),
|
|
24
|
-
)
|
|
25
35
|
const optionsIdentity = getDataSourceQueryOptionsIdentity(options)
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const isInitialized = host.status === "initialized"
|
|
36
|
+
const [state, setState] = useState<DataSourceHookState>(() => ({
|
|
37
|
+
key,
|
|
38
|
+
optionsIdentity,
|
|
39
|
+
snapshot: EMPTY_DATA_SOURCE_RESULT,
|
|
40
|
+
}))
|
|
32
41
|
// biome-ignore lint/correctness/useExhaustiveDependencies(options): Compare options by value through optionsIdentity. Object identity alone must not trigger queries.
|
|
33
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies(optionsIdentity): Changes to the serialized options must trigger a query even though the callback reads options.
|
|
34
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies(matchingDataSource): Requery when the matching data source changes, even when the key and options are unchanged.
|
|
35
42
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
[subscriptionId],
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
const view = customBlockDataSources.getView(host, key, subscriptionId)
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
items: view.items,
|
|
54
|
-
collectionSchema: view.collectionSchema,
|
|
55
|
-
propertySchemasById: view.propertySchemasById,
|
|
56
|
-
propertyIdsByKey: view.propertyIdsByKey,
|
|
57
|
-
propertySchemasByKey: view.propertySchemasByKey,
|
|
58
|
-
isLoading: view.isLoading,
|
|
59
|
-
hasMore: view.hasMore,
|
|
60
|
-
error: view.error,
|
|
43
|
+
return customBlock.subscribeToDataSource({
|
|
44
|
+
key,
|
|
45
|
+
onSnapshot: snapshot => {
|
|
46
|
+
setState({ key, optionsIdentity, snapshot })
|
|
47
|
+
},
|
|
48
|
+
options,
|
|
49
|
+
})
|
|
50
|
+
}, [key, optionsIdentity])
|
|
51
|
+
if (state.key !== key || state.optionsIdentity !== optionsIdentity) {
|
|
52
|
+
return EMPTY_DATA_SOURCE_RESULT
|
|
61
53
|
}
|
|
54
|
+
return state.snapshot
|
|
62
55
|
}
|
package/src/types.ts
CHANGED
|
@@ -80,7 +80,7 @@ export type NotionPagePropertyInputMap = {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Consumer-facing page shape returned from
|
|
83
|
+
* Consumer-facing page shape returned from a data source snapshot. Derived from the bridge payload
|
|
84
84
|
* plus the data source's `propertyIdsByKey`.
|
|
85
85
|
*/
|
|
86
86
|
export type NotionDataSourcePage = {
|
|
@@ -106,12 +106,20 @@ export type NotionDataSourcePage = {
|
|
|
106
106
|
* keys to raw property IDs before sending the bridge message to the host.
|
|
107
107
|
*/
|
|
108
108
|
update: (args: NotionDataSourcePageUpdateArgs) => Promise<UpdatePageResult>
|
|
109
|
+
/** Archives this page. Shorthand for update({ is_archived: true }). */
|
|
110
|
+
archive: () => Promise<UpdatePageResult>
|
|
111
|
+
/** Unarchives this page. Shorthand for update({ is_archived: false }). */
|
|
112
|
+
unarchive: () => Promise<UpdatePageResult>
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
export type NotionDataSourcePageUpdateArgs = {
|
|
112
116
|
properties?: NotionPagePropertyInputMap
|
|
113
117
|
icon?: NotionPageIcon
|
|
114
118
|
cover?: NotionPageCover
|
|
119
|
+
/** Whether to archive this page. Takes precedence over archived. */
|
|
120
|
+
is_archived?: boolean
|
|
121
|
+
/** @deprecated Use is_archived instead. */
|
|
122
|
+
// TODO(custom-blocks): Remove archived when upgrading to bridge protocol v4.
|
|
115
123
|
archived?: boolean
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -168,14 +176,14 @@ export type NotionDataSourceFilter =
|
|
|
168
176
|
| { and: NotionDataSourcePropertyFilter[] }
|
|
169
177
|
|
|
170
178
|
/**
|
|
171
|
-
*
|
|
179
|
+
* Latest live snapshot of a data source query.
|
|
172
180
|
*
|
|
173
181
|
* - `items` — the rows the host has returned so far. Empty until the first response arrives.
|
|
174
182
|
* - `isLoading` — `true` while a query is in flight.
|
|
175
|
-
* - `hasMore` — `true` if the host indicated more rows are available beyond the
|
|
183
|
+
* - `hasMore` — `true` if the host indicated more rows are available beyond the requested prefix.
|
|
176
184
|
* - `error` — structured error information if the most recent query failed.
|
|
177
185
|
*/
|
|
178
|
-
export type
|
|
186
|
+
export type DataSourceSnapshot = {
|
|
179
187
|
items: NotionDataSourcePage[]
|
|
180
188
|
/**
|
|
181
189
|
* Collection/data source schema for the bound Notion data source, including raw property
|
|
@@ -203,22 +211,25 @@ export type UseDataSourceResult = {
|
|
|
203
211
|
error?: CustomBlockQueryDataSourceErrorInfo
|
|
204
212
|
}
|
|
205
213
|
|
|
206
|
-
export type
|
|
207
|
-
/**
|
|
208
|
-
* Maximum number of rows to request from the host. Defaults to 20.
|
|
209
|
-
*/
|
|
214
|
+
export type DataSourceQueryOptions = {
|
|
215
|
+
/** Maximum number of rows to request from the host. Defaults to 20. */
|
|
210
216
|
limit?: number
|
|
211
|
-
/**
|
|
212
|
-
* Optional property filter. The SDK resolves semantic property keys before
|
|
213
|
-
* it sends the query to the host.
|
|
214
|
-
*/
|
|
217
|
+
/** Optional property filter. The SDK resolves semantic keys before it sends the query. */
|
|
215
218
|
filter?: NotionDataSourceFilter
|
|
216
|
-
/**
|
|
217
|
-
* Optional property sorts. The host applies them in array order.
|
|
218
|
-
*/
|
|
219
|
+
/** Optional property sorts. The host applies them in array order. */
|
|
219
220
|
sorts?: NotionDataSourceSort[]
|
|
220
221
|
}
|
|
221
222
|
|
|
223
|
+
export type SubscribeToDataSourceArgs = {
|
|
224
|
+
key: string
|
|
225
|
+
onSnapshot: (snapshot: DataSourceSnapshot) => void
|
|
226
|
+
options?: DataSourceQueryOptions
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export type UseDataSourceResult = DataSourceSnapshot
|
|
230
|
+
|
|
231
|
+
export type UseDataSourceOptions = DataSourceQueryOptions
|
|
232
|
+
|
|
222
233
|
/**
|
|
223
234
|
* Parent reference accepted by `sdk.pages.create`. Mirrors Notion's public `POST /v1/pages`
|
|
224
235
|
* parent shape; see https://developers.notion.com/reference/data-source.
|
|
@@ -260,7 +271,16 @@ export type CreatePageResult = BridgeMessagePayload<
|
|
|
260
271
|
CustomBlockCreatePageErrorInfo
|
|
261
272
|
>
|
|
262
273
|
|
|
263
|
-
export type UpdatePageArgs = Omit<
|
|
274
|
+
export type UpdatePageArgs = Omit<
|
|
275
|
+
UpdatePageMessage,
|
|
276
|
+
"type" | "requestId" | "archived"
|
|
277
|
+
> & {
|
|
278
|
+
/** Whether to archive this page. Takes precedence over archived. */
|
|
279
|
+
is_archived?: boolean
|
|
280
|
+
/** @deprecated Use is_archived instead. */
|
|
281
|
+
// TODO(custom-blocks): Remove archived when upgrading to bridge protocol v4.
|
|
282
|
+
archived?: boolean
|
|
283
|
+
}
|
|
264
284
|
|
|
265
285
|
/**
|
|
266
286
|
* @deprecated Use `UpdatePageArgs` instead.
|
|
@@ -278,7 +298,7 @@ export type GetPageResult = BridgeMessagePayload<
|
|
|
278
298
|
>
|
|
279
299
|
|
|
280
300
|
/**
|
|
281
|
-
* Result of `sdk.pages.update`
|
|
301
|
+
* Result of `sdk.pages.update`, `sdk.pages.archive`, and `sdk.pages.unarchive`.
|
|
282
302
|
*/
|
|
283
303
|
export type UpdatePageResult = BridgeMessagePayload<
|
|
284
304
|
UpdatePageResultMessage,
|