@sanity/sdk-react 2.5.0 → 2.6.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.
@@ -1,10 +1,9 @@
1
1
  import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
2
- import {type FrameMessage} from '@sanity/sdk'
2
+ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
3
3
  import {useCallback} from 'react'
4
4
 
5
5
  import {useWindowConnection} from '../comlink/useWindowConnection'
6
- import {type DocumentHandleWithSource} from './types'
7
- import {getResourceIdFromDocumentHandle} from './utils/getResourceIdFromDocumentHandle'
6
+ import {useResourceIdFromDocumentHandle} from './utils/useResourceIdFromDocumentHandle'
8
7
 
9
8
  /**
10
9
  * Message type for sending intents to the dashboard
@@ -42,7 +41,7 @@ interface DispatchIntent {
42
41
  interface UseDispatchIntentParams {
43
42
  action?: 'edit'
44
43
  intentId?: string
45
- documentHandle: DocumentHandleWithSource
44
+ documentHandle: DocumentHandle
46
45
  parameters?: Record<string, unknown>
47
46
  }
48
47
 
@@ -104,13 +103,15 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
104
103
  connectTo: SDK_CHANNEL_NAME,
105
104
  })
106
105
 
106
+ const resource = useResourceIdFromDocumentHandle(documentHandle)
107
+
107
108
  const dispatchIntent = useCallback(() => {
108
109
  try {
109
110
  if (!action && !intentId) {
110
111
  throw new Error('useDispatchIntent: Either `action` or `intentId` must be provided.')
111
112
  }
112
113
 
113
- const {projectId, dataset, source} = documentHandle
114
+ const {projectId, dataset, sourceName} = documentHandle
114
115
 
115
116
  if (action && intentId) {
116
117
  // eslint-disable-next-line no-console -- warn if both action and intentId are provided
@@ -119,14 +120,12 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
119
120
  )
120
121
  }
121
122
 
122
- if (!source && (!projectId || !dataset)) {
123
+ if (!sourceName && (!projectId || !dataset)) {
123
124
  throw new Error(
124
- 'useDispatchIntent: Either `source` or both `projectId` and `dataset` must be provided in documentHandle.',
125
+ 'useDispatchIntent: Either `sourceName` or both `projectId` and `dataset` must be provided in documentHandle.',
125
126
  )
126
127
  }
127
128
 
128
- const resource = getResourceIdFromDocumentHandle(documentHandle)
129
-
130
129
  const message: IntentMessage = {
131
130
  type: 'dashboard/v1/events/intents/dispatch-intent',
132
131
  data: {
@@ -150,7 +149,7 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
150
149
  console.error('Failed to dispatch intent:', error)
151
150
  throw error
152
151
  }
153
- }, [action, intentId, documentHandle, parameters, sendMessage])
152
+ }, [action, intentId, documentHandle, parameters, sendMessage, resource.id, resource.type])
154
153
 
155
154
  return {
156
155
  dispatchIntent,
@@ -1,28 +1,22 @@
1
- import {
2
- canvasSource,
3
- datasetSource,
4
- type DocumentHandle,
5
- type DocumentSource,
6
- mediaLibrarySource,
7
- } from '@sanity/sdk'
1
+ import {type DocumentHandle} from '@sanity/sdk'
8
2
  import {describe, expect, it} from 'vitest'
9
3
 
10
- import {type DocumentHandleWithSource} from '../types'
11
- import {getResourceIdFromDocumentHandle} from './getResourceIdFromDocumentHandle'
4
+ import {renderHook} from '../../../../test/test-utils'
5
+ import {useResourceIdFromDocumentHandle} from './useResourceIdFromDocumentHandle'
12
6
 
13
7
  describe('getResourceIdFromDocumentHandle', () => {
14
8
  describe('with traditional DocumentHandle (projectId/dataset)', () => {
15
9
  it('should return resource ID from projectId and dataset', () => {
16
- const documentHandle: DocumentHandle = {
10
+ const documentHandle = {
17
11
  documentId: 'test-document-id',
18
12
  documentType: 'test-document-type',
19
13
  projectId: 'test-project-id',
20
14
  dataset: 'test-dataset',
21
15
  }
22
16
 
23
- const result = getResourceIdFromDocumentHandle(documentHandle)
17
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
24
18
 
25
- expect(result).toEqual({
19
+ expect(result.current).toEqual({
26
20
  id: 'test-project-id.test-dataset',
27
21
  type: undefined,
28
22
  })
@@ -31,33 +25,33 @@ describe('getResourceIdFromDocumentHandle', () => {
31
25
 
32
26
  describe('with DocumentHandleWithSource - media library', () => {
33
27
  it('should return media library ID and resourceType when media library source is provided', () => {
34
- const documentHandle: DocumentHandleWithSource = {
28
+ const documentHandle: DocumentHandle = {
35
29
  documentId: 'test-asset-id',
36
30
  documentType: 'sanity.asset',
37
- source: mediaLibrarySource('mlPGY7BEqt52'),
31
+ sourceName: 'media-library',
38
32
  }
39
33
 
40
- const result = getResourceIdFromDocumentHandle(documentHandle)
34
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
41
35
 
42
- expect(result).toEqual({
43
- id: 'mlPGY7BEqt52',
36
+ expect(result.current).toEqual({
37
+ id: 'media-library-id',
44
38
  type: 'media-library',
45
39
  })
46
40
  })
47
41
 
48
42
  it('should prioritize source over projectId/dataset when both are provided', () => {
49
- const documentHandle: DocumentHandleWithSource = {
43
+ const documentHandle = {
50
44
  documentId: 'test-asset-id',
51
45
  documentType: 'sanity.asset',
52
46
  projectId: 'test-project-id',
53
47
  dataset: 'test-dataset',
54
- source: mediaLibrarySource('mlPGY7BEqt52'),
48
+ sourceName: 'media-library',
55
49
  }
56
50
 
57
- const result = getResourceIdFromDocumentHandle(documentHandle)
51
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
58
52
 
59
- expect(result).toEqual({
60
- id: 'mlPGY7BEqt52',
53
+ expect(result.current).toEqual({
54
+ id: 'media-library-id',
61
55
  type: 'media-library',
62
56
  })
63
57
  })
@@ -65,16 +59,16 @@ describe('getResourceIdFromDocumentHandle', () => {
65
59
 
66
60
  describe('with DocumentHandleWithSource - canvas', () => {
67
61
  it('should return canvas ID and resourceType when canvas source is provided', () => {
68
- const documentHandle: DocumentHandleWithSource = {
62
+ const documentHandle = {
69
63
  documentId: 'test-canvas-document-id',
70
64
  documentType: 'sanity.canvas.document',
71
- source: canvasSource('canvas123'),
65
+ sourceName: 'canvas',
72
66
  }
73
67
 
74
- const result = getResourceIdFromDocumentHandle(documentHandle)
68
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
75
69
 
76
- expect(result).toEqual({
77
- id: 'canvas123',
70
+ expect(result.current).toEqual({
71
+ id: 'canvas-id',
78
72
  type: 'canvas',
79
73
  })
80
74
  })
@@ -82,32 +76,32 @@ describe('getResourceIdFromDocumentHandle', () => {
82
76
 
83
77
  describe('with DocumentHandleWithSource - dataset source', () => {
84
78
  it('should return dataset resource ID when dataset source is provided', () => {
85
- const documentHandle: DocumentHandleWithSource = {
79
+ const documentHandle = {
86
80
  documentId: 'test-document-id',
87
81
  documentType: 'test-document-type',
88
- source: datasetSource('source-project-id', 'source-dataset'),
82
+ sourceName: 'dataset',
89
83
  }
90
84
 
91
- const result = getResourceIdFromDocumentHandle(documentHandle)
85
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
92
86
 
93
- expect(result).toEqual({
87
+ expect(result.current).toEqual({
94
88
  id: 'source-project-id.source-dataset',
95
89
  type: undefined,
96
90
  })
97
91
  })
98
92
 
99
93
  it('should use dataset source over projectId/dataset when both are provided', () => {
100
- const documentHandle: DocumentHandleWithSource = {
94
+ const documentHandle = {
101
95
  documentId: 'test-document-id',
102
96
  documentType: 'test-document-type',
103
97
  projectId: 'test-project-id',
104
98
  dataset: 'test-dataset',
105
- source: datasetSource('source-project-id', 'source-dataset'),
99
+ sourceName: 'dataset',
106
100
  }
107
101
 
108
- const result = getResourceIdFromDocumentHandle(documentHandle)
102
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
109
103
 
110
- expect(result).toEqual({
104
+ expect(result.current).toEqual({
111
105
  id: 'source-project-id.source-dataset',
112
106
  type: undefined,
113
107
  })
@@ -116,37 +110,17 @@ describe('getResourceIdFromDocumentHandle', () => {
116
110
 
117
111
  describe('edge cases', () => {
118
112
  it('should handle DocumentHandleWithSource with undefined source', () => {
119
- const documentHandle: DocumentHandleWithSource = {
120
- documentId: 'test-document-id',
121
- documentType: 'test-document-type',
122
- projectId: 'test-project-id',
123
- dataset: 'test-dataset',
124
- source: undefined,
125
- }
126
-
127
- const result = getResourceIdFromDocumentHandle(documentHandle)
128
-
129
- expect(result).toEqual({
130
- id: 'test-project-id.test-dataset',
131
- type: undefined,
132
- })
133
- })
134
-
135
- it('should fall back to projectId/dataset when source is not recognized', () => {
136
- const documentHandle: DocumentHandleWithSource = {
113
+ const documentHandle = {
137
114
  documentId: 'test-document-id',
138
115
  documentType: 'test-document-type',
139
116
  projectId: 'test-project-id',
140
117
  dataset: 'test-dataset',
141
- source: {
142
- __sanity_internal_sourceId: 'unknown-format',
143
- } as unknown as DocumentSource,
118
+ sourceName: undefined,
144
119
  }
145
120
 
146
- const result = getResourceIdFromDocumentHandle(documentHandle)
121
+ const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
147
122
 
148
- // Falls back to projectId.dataset when source format is not recognized
149
- expect(result).toEqual({
123
+ expect(result.current).toEqual({
150
124
  id: 'test-project-id.test-dataset',
151
125
  type: undefined,
152
126
  })
@@ -0,0 +1,46 @@
1
+ import {
2
+ type DocumentHandle,
3
+ isCanvasSource,
4
+ isDatasetSource,
5
+ isMediaLibrarySource,
6
+ } from '@sanity/sdk'
7
+
8
+ import {useSource} from '../../context/useSource'
9
+
10
+ interface DashboardMessageResource {
11
+ id: string
12
+ type?: 'media-library' | 'canvas'
13
+ }
14
+ /** Currently only used for dispatching intents to the dashboard,
15
+ * but could easily be extended to other dashboard hooks
16
+ * @beta
17
+ */
18
+ export function useResourceIdFromDocumentHandle(
19
+ documentHandle: DocumentHandle,
20
+ ): DashboardMessageResource {
21
+ const source = useSource(documentHandle)
22
+ const {projectId, dataset} = documentHandle
23
+ let resourceId: string = ''
24
+ let resourceType: 'media-library' | 'canvas' | undefined
25
+ if (projectId && dataset) {
26
+ resourceId = `${projectId}.${dataset}`
27
+ }
28
+
29
+ if (source) {
30
+ if (isDatasetSource(source)) {
31
+ resourceId = `${source.projectId}.${source.dataset}`
32
+ resourceType = undefined
33
+ } else if (isMediaLibrarySource(source)) {
34
+ resourceId = source.mediaLibraryId
35
+ resourceType = 'media-library'
36
+ } else if (isCanvasSource(source)) {
37
+ resourceId = source.canvasId
38
+ resourceType = 'canvas'
39
+ }
40
+ }
41
+
42
+ return {
43
+ id: resourceId,
44
+ type: resourceType,
45
+ }
46
+ }
@@ -100,6 +100,9 @@ export function useEditDocument<TData>(
100
100
  * 3. **Explicit Type (Full Document):** Edit the entire document with a manually specified type.
101
101
  * 4. **Explicit Type (Specific Path):** Edit a specific field with a manually specified type.
102
102
  *
103
+ * **LiveEdit Documents:**
104
+ * For documents using {@link DocumentHandle.liveEdit | liveEdit mode} (set via `liveEdit: true` in the document handle), edits are applied directly to the published document without creating a draft.
105
+ *
103
106
  * This hook relies on the document state being loaded. If the document is not yet available
104
107
  * (e.g., during initial load), the component using this hook will suspend.
105
108
  *
@@ -23,8 +23,8 @@ export interface DocumentsOptions<
23
23
  TDocumentType extends string = string,
24
24
  TDataset extends string = string,
25
25
  TProjectId extends string = string,
26
- > extends DatasetHandle<TDataset, TProjectId>,
27
- Pick<QueryOptions, 'perspective' | 'params'> {
26
+ >
27
+ extends DatasetHandle<TDataset, TProjectId>, Pick<QueryOptions, 'perspective' | 'params'> {
28
28
  /**
29
29
  * Filter documents by their `_type`. Can be a single type or an array of types.
30
30
  */
@@ -39,6 +39,7 @@ export interface DocumentsOptions<
39
39
  batchSize?: number
40
40
  /**
41
41
  * Sorting configuration for the results
42
+ * @beta
42
43
  */
43
44
  orderings?: SortOrderingItem[]
44
45
  /**
@@ -28,6 +28,7 @@ export interface PaginatedDocumentsOptions<
28
28
  pageSize?: number
29
29
  /**
30
30
  * Sorting configuration for the results
31
+ * @beta
31
32
  */
32
33
  orderings?: SortOrderingItem[]
33
34
  /**
@@ -9,6 +9,16 @@ import {type SanityQueryResult} from 'groq'
9
9
  import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
10
10
 
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
+ import {useSource} from '../context/useSource'
13
+
14
+ interface UseQueryOptions<
15
+ TQuery extends string = string,
16
+ TDataset extends string = string,
17
+ TProjectId extends string = string,
18
+ TSourceName extends string = string,
19
+ > extends QueryOptions<TQuery, TDataset, TProjectId> {
20
+ sourceName?: TSourceName
21
+ }
12
22
 
13
23
  // Overload 1: Inferred Type (using Typegen)
14
24
  /**
@@ -70,8 +80,9 @@ export function useQuery<
70
80
  TQuery extends string = string,
71
81
  TDataset extends string = string,
72
82
  TProjectId extends string = string,
83
+ TSourceName extends string = string,
73
84
  >(
74
- options: QueryOptions<TQuery, TDataset, TProjectId>,
85
+ options: UseQueryOptions<TQuery, TDataset, TProjectId, TSourceName>,
75
86
  ): {
76
87
  /** The query result, typed based on the GROQ query string */
77
88
  data: SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>
@@ -137,6 +148,8 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
137
148
  // Implementation returns unknown, overloads define specifics
138
149
  const instance = useSanityInstance(options)
139
150
 
151
+ const source = useSource(options)
152
+
140
153
  // Use React's useTransition to avoid UI jank when queries change
141
154
  const [isPending, startTransition] = useTransition()
142
155
 
@@ -144,8 +157,6 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
144
157
  const queryKey = getQueryKey(options)
145
158
  // Use a deferred state to avoid immediate re-renders when the query changes
146
159
  const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
147
- // Parse the deferred query key back into a query and options
148
- const deferred = useMemo(() => parseQueryKey(deferredQueryKey), [deferredQueryKey])
149
160
 
150
161
  // Create an AbortController to cancel in-flight requests when needed
151
162
  const ref = useRef<AbortController>(new AbortController())
@@ -166,10 +177,11 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
166
177
  }, [deferredQueryKey, queryKey])
167
178
 
168
179
  // Get the state source for this query from the query store
169
- const {getCurrent, subscribe} = useMemo(
170
- () => getQueryState(instance, deferred),
171
- [instance, deferred],
172
- )
180
+ // Memoize the options object by depending on the stable string key instead of the parsed object
181
+ const {getCurrent, subscribe} = useMemo(() => {
182
+ const deferred = parseQueryKey(deferredQueryKey)
183
+ return getQueryState(instance, {...deferred, source})
184
+ }, [instance, deferredQueryKey, source])
173
185
 
174
186
  // If data isn't available yet, suspend rendering
175
187
  if (getCurrent() === undefined) {
@@ -182,8 +194,9 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
182
194
  // the captured signal remains unchanged for this suspended render.
183
195
  // Thus, the promise thrown here uses a stable abort signal, ensuring correct behavior.
184
196
  const currentSignal = ref.current.signal
197
+ const deferred = parseQueryKey(deferredQueryKey)
185
198
 
186
- throw resolveQuery(instance, {...deferred, signal: currentSignal})
199
+ throw resolveQuery(instance, {...deferred, source, signal: currentSignal})
187
200
  }
188
201
 
189
202
  // Subscribe to updates and get the current data
@@ -1,12 +0,0 @@
1
- import {type DocumentHandle, type DocumentSource} from '@sanity/sdk'
2
- /**
3
- * Document handle that optionally includes a source (e.g., media library source)
4
- * or projectId and dataset for traditional dataset sources
5
- * (but now marked optional since it's valid to just use a source)
6
- * @beta
7
- */
8
- export interface DocumentHandleWithSource extends Omit<DocumentHandle, 'projectId' | 'dataset'> {
9
- source?: DocumentSource
10
- projectId?: string
11
- dataset?: string
12
- }
@@ -1,53 +0,0 @@
1
- import {type DocumentHandle, type DocumentSource} from '@sanity/sdk'
2
-
3
- import {type DocumentHandleWithSource} from '../types'
4
-
5
- // Internal constant for accessing source ID
6
- const SOURCE_ID = '__sanity_internal_sourceId' as const
7
-
8
- interface DashboardMessageResource {
9
- id: string
10
- type?: 'media-library' | 'canvas'
11
- }
12
-
13
- const isDocumentHandleWithSource = (
14
- documentHandle: DocumentHandle | DocumentHandleWithSource,
15
- ): documentHandle is DocumentHandleWithSource => {
16
- return 'source' in documentHandle
17
- }
18
-
19
- /** Currently only used for dispatching intents to the dashboard,
20
- * but could easily be extended to other dashboard hooks
21
- * @beta
22
- */
23
- export function getResourceIdFromDocumentHandle(
24
- documentHandle: DocumentHandle | DocumentHandleWithSource,
25
- ): DashboardMessageResource {
26
- let source: DocumentSource | undefined
27
-
28
- const {projectId, dataset} = documentHandle
29
- if (isDocumentHandleWithSource(documentHandle)) {
30
- source = documentHandle.source
31
- }
32
- let resourceId: string = projectId + '.' + dataset
33
- let resourceType: 'media-library' | 'canvas' | undefined
34
-
35
- if (source) {
36
- const sourceId = (source as Record<string, unknown>)[SOURCE_ID]
37
- if (Array.isArray(sourceId)) {
38
- if (sourceId[0] === 'media-library' || sourceId[0] === 'canvas') {
39
- resourceType = sourceId[0] as 'media-library' | 'canvas'
40
- resourceId = sourceId[1] as string
41
- }
42
- } else if (sourceId && typeof sourceId === 'object' && 'projectId' in sourceId) {
43
- const datasetSource = sourceId as {projectId: string; dataset: string}
44
- // don't create type since it's ambiguous for project / dataset docs
45
- resourceId = `${datasetSource.projectId}.${datasetSource.dataset}`
46
- }
47
- }
48
-
49
- return {
50
- id: resourceId,
51
- type: resourceType,
52
- }
53
- }