@sanity/sdk-react 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/index.d.ts +232 -47
  2. package/dist/index.js +468 -263
  3. package/dist/index.js.map +1 -1
  4. package/package.json +8 -10
  5. package/src/_exports/sdk-react.ts +5 -0
  6. package/src/components/SDKProvider.tsx +36 -8
  7. package/src/components/SanityApp.tsx +3 -2
  8. package/src/components/auth/AuthBoundary.tsx +8 -1
  9. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  10. package/src/components/auth/LoginError.test.tsx +191 -5
  11. package/src/components/auth/LoginError.tsx +100 -56
  12. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  13. package/src/components/errors/ChunkLoadError.tsx +56 -0
  14. package/src/components/errors/chunkReloadStorage.ts +57 -0
  15. package/src/context/ResourceProvider.test.tsx +7 -1
  16. package/src/context/ResourceProvider.tsx +11 -4
  17. package/src/context/ResourcesContext.tsx +7 -0
  18. package/src/context/SDKStudioContext.ts +6 -0
  19. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  20. package/src/context/SanityInstanceProvider.tsx +71 -0
  21. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  22. package/src/hooks/dashboard/useDispatchIntent.test.ts +8 -6
  23. package/src/hooks/dashboard/useDispatchIntent.ts +6 -6
  24. package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
  25. package/src/hooks/dashboard/useWindowTitle.ts +112 -0
  26. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
  27. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -13
  28. package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
  29. package/src/hooks/document/useApplyDocumentActions.ts +99 -3
  30. package/src/hooks/document/useDocument.ts +22 -6
  31. package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
  32. package/src/hooks/document/useDocumentEvent.ts +10 -3
  33. package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
  34. package/src/hooks/document/useDocumentPermissions.ts +22 -0
  35. package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
  36. package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
  37. package/src/hooks/document/useEditDocument.ts +34 -8
  38. package/src/hooks/documents/useDocuments.ts +11 -6
  39. package/src/hooks/helpers/useNormalizedResourceOptions.ts +131 -0
  40. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  41. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +11 -8
  42. package/src/hooks/presence/usePresence.test.tsx +56 -9
  43. package/src/hooks/presence/usePresence.ts +25 -4
  44. package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
  45. package/src/hooks/preview/useDocumentPreview.tsx +40 -55
  46. package/src/hooks/projection/useDocumentProjection.ts +8 -6
  47. package/src/hooks/query/useQuery.ts +12 -9
  48. package/src/hooks/releases/useActiveReleases.ts +32 -13
  49. package/src/hooks/releases/usePerspective.ts +26 -14
  50. package/src/hooks/users/useUser.ts +2 -0
  51. package/src/hooks/users/useUsers.ts +2 -0
  52. package/src/context/SourcesContext.tsx +0 -7
  53. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -85
@@ -5,8 +5,14 @@ import {
5
5
  type DocumentAction,
6
6
  } from '@sanity/sdk'
7
7
  import {type SanityDocument} from 'groq'
8
+ import {useContext} from 'react'
8
9
 
10
+ import {ResourcesContext} from '../../context/ResourcesContext'
9
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
+ import {
13
+ normalizeResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
10
16
  // this import is used in an `{@link useEditDocument}`
11
17
  // eslint-disable-next-line unused-imports/no-unused-imports, import/consistent-type-specifier-style
12
18
  import type {useEditDocument} from './useEditDocument'
@@ -23,7 +29,7 @@ interface UseApplyDocumentActions {
23
29
  action:
24
30
  | DocumentAction<TDocumentType, TDataset, TProjectId>
25
31
  | DocumentAction<TDocumentType, TDataset, TProjectId>[],
26
- options?: ApplyDocumentActionsOptions,
32
+ options?: WithResourceNameSupport<ApplyDocumentActionsOptions>,
27
33
  ) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
28
34
  }
29
35
 
@@ -149,17 +155,85 @@ interface UseApplyDocumentActions {
149
155
  * return <button onClick={handleCreateArticle}>Create Article</button>
150
156
  * }
151
157
  * ```
158
+ *
159
+ * @example Create a new document in a release
160
+ * ```tsx
161
+ * import {
162
+ * createDocument,
163
+ * createDocumentHandle,
164
+ * useApplyDocumentActions
165
+ * } from '@sanity/sdk-react'
166
+ *
167
+ * function CreateArticleButton() {
168
+ * const apply = useApplyDocumentActions()
169
+ *
170
+ * const handleCreateArticle = () => {
171
+ * // Use any valid document ID — not the internal `versions.<releaseName>.<id>` format or "drafts.<id>" format.
172
+ * // New documents must be explicitly created with `createDocument` to become part of a release.
173
+ * const newDocHandle = createDocumentHandle({
174
+ * documentId: crypto.randomUUID(), // or the existing document ID you want to make a release version of
175
+ * documentType: 'article',
176
+ * perspective: {releaseName: 'summer-drop'},
177
+ * })
178
+ *
179
+ * apply(
180
+ * createDocument(newDocHandle, {
181
+ * title: 'New Article',
182
+ * author: 'John Doe',
183
+ * publishedAt: new Date().toISOString(),
184
+ * })
185
+ * )
186
+ * }
187
+ *
188
+ * return <button onClick={handleCreateArticle}>Create Article</button>
189
+ * }
190
+ * ```
191
+ *
192
+ * @example Edit an existing document in a release
193
+ * ```tsx
194
+ * import {
195
+ * editDocument,
196
+ * createDocumentHandle,
197
+ * useApplyDocumentActions
198
+ * } from '@sanity/sdk-react'
199
+ *
200
+ * function EditArticleInReleaseButton({documentId}: {documentId: string}) {
201
+ * const apply = useApplyDocumentActions()
202
+ *
203
+ * const handleEdit = () => {
204
+ * // Pass the document's regular ID — not `versions.<releaseName>.<id>`.
205
+ * // Documents that already have a version in the release can be edited directly with `editDocument`.
206
+ * const docHandle = createDocumentHandle({
207
+ * documentId,
208
+ * documentType: 'article',
209
+ * perspective: {releaseName: 'summer-drop'},
210
+ * })
211
+ *
212
+ * apply(editDocument(docHandle, {title: 'Updated for release'}))
213
+ * }
214
+ *
215
+ * return <button onClick={handleEdit}>Edit in Release</button>
216
+ * }
217
+ * ```
152
218
  */
153
219
  export const useApplyDocumentActions: UseApplyDocumentActions = () => {
154
220
  const instance = useSanityInstance()
221
+ const resources = useContext(ResourcesContext)
155
222
 
156
223
  return (actionOrActions, options) => {
157
224
  const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
225
+ const normalizedOptions = options ? normalizeResourceOptions(options, resources) : undefined
158
226
 
159
227
  let projectId
160
228
  let dataset
229
+ let resource
161
230
  for (const action of actions) {
162
231
  if (action.projectId) {
232
+ if (resource) {
233
+ throw new Error(
234
+ `Mismatches between projectId/dataset options and resource in actions. Found projectId "${action.projectId}" and dataset "${action.dataset}" but expected resource "${resource}".`,
235
+ )
236
+ }
163
237
  if (!projectId) projectId = action.projectId
164
238
  if (action.projectId !== projectId) {
165
239
  throw new Error(
@@ -176,6 +250,20 @@ export const useApplyDocumentActions: UseApplyDocumentActions = () => {
176
250
  }
177
251
  }
178
252
  }
253
+
254
+ if (action.resource) {
255
+ if (!resource) resource = action.resource
256
+ if (action.resource !== resource) {
257
+ throw new Error(
258
+ `Mismatched resources found in actions. All actions must belong to the same resource. Found "${action.resource}" but expected "${resource}".`,
259
+ )
260
+ }
261
+ if (projectId || dataset) {
262
+ throw new Error(
263
+ `Mismatches between projectId/dataset options and resource in actions. Found "${action.resource}" but expected project "${projectId}" and dataset "${dataset}".`,
264
+ )
265
+ }
266
+ }
179
267
  }
180
268
 
181
269
  if (projectId || dataset) {
@@ -187,9 +275,17 @@ export const useApplyDocumentActions: UseApplyDocumentActions = () => {
187
275
  )
188
276
  }
189
277
 
190
- return applyDocumentActions(actualInstance, {actions, ...options})
278
+ return applyDocumentActions(actualInstance, {
279
+ actions,
280
+ resource,
281
+ ...normalizedOptions,
282
+ })
191
283
  }
192
284
 
193
- return applyDocumentActions(instance, {actions, ...options})
285
+ return applyDocumentActions(instance, {
286
+ actions,
287
+ resource,
288
+ ...normalizedOptions,
289
+ })
194
290
  }
195
291
  }
@@ -3,6 +3,11 @@ import {type SanityDocument} from 'groq'
3
3
  import {identity} from 'rxjs'
4
4
 
5
5
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
6
+ import {
7
+ useNormalizedResourceOptions,
8
+ type WithResourceNameSupport,
9
+ } from '../helpers/useNormalizedResourceOptions'
10
+ import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
6
11
  // used in an `{@link useDocumentProjection}` and `{@link useQuery}`
7
12
  // eslint-disable-next-line import/consistent-type-specifier-style, unused-imports/no-unused-imports
8
13
  import type {useDocumentProjection} from '../projection/useDocumentProjection'
@@ -33,10 +38,17 @@ const wrapHookWithData = <TParams extends unknown[], TReturn>(
33
38
  return useHook
34
39
  }
35
40
 
41
+ type UseDocumentOptions<
42
+ TPath extends string | undefined = undefined,
43
+ TDocumentType extends string = string,
44
+ TDataset extends string = string,
45
+ TProjectId extends string = string,
46
+ > = WithResourceNameSupport<DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>>
47
+
36
48
  interface UseDocument {
37
49
  /** @internal */
38
50
  <TDocumentType extends string, TDataset extends string, TProjectId extends string = string>(
39
- options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
51
+ options: UseDocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
40
52
  ): {data: SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null}
41
53
 
42
54
  /** @internal */
@@ -46,7 +58,7 @@ interface UseDocument {
46
58
  TDataset extends string = string,
47
59
  TProjectId extends string = string,
48
60
  >(
49
- options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
61
+ options: UseDocumentOptions<TPath, TDocumentType>,
50
62
  ): {
51
63
  data: JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath> | undefined
52
64
  }
@@ -124,7 +136,7 @@ interface UseDocument {
124
136
  TDataset extends string = string,
125
137
  TProjectId extends string = string,
126
138
  >(
127
- options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
139
+ options: UseDocumentOptions<TPath, TDocumentType>,
128
140
  ): TPath extends string
129
141
  ? {
130
142
  data:
@@ -194,13 +206,13 @@ interface UseDocument {
194
206
  * @inlineType DocumentOptions
195
207
  */
196
208
  <TData, TPath extends string>(
197
- options: DocumentOptions<TPath>,
209
+ options: UseDocumentOptions<TPath>,
198
210
  ): TPath extends string ? {data: TData | undefined} : {data: TData | null}
199
211
 
200
212
  /**
201
213
  * @internal
202
214
  */
203
- (options: DocumentOptions): {data: unknown}
215
+ (options: UseDocumentOptions): {data: unknown}
204
216
  }
205
217
 
206
218
  /**
@@ -226,4 +238,8 @@ interface UseDocument {
226
238
  *
227
239
  * @function
228
240
  */
229
- export const useDocument = wrapHookWithData(useDocumentValue) as UseDocument
241
+ export const useDocument = wrapHookWithData((options: UseDocumentOptions) => {
242
+ useTrackHookUsage('useDocument')
243
+ const normalizedOptions = useNormalizedResourceOptions(options)
244
+ return useDocumentValue(normalizedOptions)
245
+ }) as UseDocument
@@ -37,11 +37,11 @@ describe('useDocumentEvent hook', () => {
37
37
  expect(vi.mocked(subscribeDocumentEvents)).toHaveBeenCalledTimes(1)
38
38
  expect(vi.mocked(subscribeDocumentEvents).mock.calls[0][0]).toEqual(expect.any(Object))
39
39
 
40
- const stableHandler = vi.mocked(subscribeDocumentEvents).mock.calls[0][1]
41
- expect(typeof stableHandler).toBe('function')
40
+ const options = vi.mocked(subscribeDocumentEvents).mock.calls[0][1]
41
+ expect(typeof options.eventHandler).toBe('function')
42
42
 
43
43
  const event = {type: 'edited', documentId: 'doc1', outgoing: {}} as DocumentEvent
44
- stableHandler(event)
44
+ options.eventHandler(event)
45
45
  expect(handleEvent).toHaveBeenCalledWith(event)
46
46
  })
47
47
 
@@ -2,6 +2,8 @@ import {type DatasetHandle, type DocumentEvent, subscribeDocumentEvents} from '@
2
2
  import {useCallback, useEffect, useInsertionEffect, useRef} from 'react'
3
3
 
4
4
  import {useSanityInstance} from '../context/useSanityInstance'
5
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
6
+ import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
5
7
 
6
8
  /**
7
9
  * @public
@@ -75,8 +77,10 @@ export function useDocumentEvent<
75
77
  // Single options object parameter
76
78
  options: UseDocumentEventOptions<TDataset, TProjectId>,
77
79
  ): void {
80
+ useTrackHookUsage('useDocumentEvent')
78
81
  // Destructure handler and datasetHandle from options
79
- const {onEvent, ...datasetHandle} = options
82
+ const normalizedOptions = useNormalizedResourceOptions(options)
83
+ const {onEvent, ...datasetHandle} = normalizedOptions
80
84
  const ref = useRef(onEvent)
81
85
 
82
86
  useInsertionEffect(() => {
@@ -89,6 +93,9 @@ export function useDocumentEvent<
89
93
 
90
94
  const instance = useSanityInstance(datasetHandle)
91
95
  useEffect(() => {
92
- return subscribeDocumentEvents(instance, stableHandler)
93
- }, [instance, stableHandler])
96
+ return subscribeDocumentEvents(instance, {
97
+ eventHandler: stableHandler,
98
+ resource: datasetHandle.resource,
99
+ })
100
+ }, [instance, datasetHandle.resource, stableHandler])
94
101
  }
@@ -1,6 +1,6 @@
1
1
  import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
2
2
  import {act, renderHook, waitFor} from '@testing-library/react'
3
- import {BehaviorSubject, firstValueFrom} from 'rxjs'
3
+ import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs'
4
4
  import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
5
5
 
6
6
  import {ResourceProvider} from '../../context/ResourceProvider'
@@ -68,7 +68,8 @@ describe('usePermissions', () => {
68
68
 
69
69
  // Set up the getPermissionsState mock
70
70
  vi.mocked(getPermissionsState).mockReturnValue({
71
- observable: permissionsSubject.asObservable(),
71
+ observable:
72
+ permissionsSubject.asObservable() as unknown as Observable<DocumentPermissionsResult>,
72
73
  subscribe: mockSubscribe,
73
74
  getCurrent: mockGetCurrent,
74
75
  })
@@ -182,6 +183,89 @@ describe('usePermissions', () => {
182
183
  }).toThrow(/Mismatched datasets found in actions/)
183
184
  })
184
185
 
186
+ it('should throw an error if actions have mismatched resources', () => {
187
+ const actions = [
188
+ {
189
+ type: 'document.publish' as const,
190
+ documentId: 'doc1',
191
+ documentType: 'article',
192
+ resource: {projectId: 'p1', dataset: 'd1'},
193
+ },
194
+ {
195
+ type: 'document.publish' as const,
196
+ documentId: 'doc2',
197
+ documentType: 'article',
198
+ resource: {projectId: 'p2', dataset: 'd2'},
199
+ },
200
+ ]
201
+
202
+ expect(() => {
203
+ renderHook(() => useDocumentPermissions(actions), {
204
+ wrapper: ({children}) => (
205
+ <ResourceProvider
206
+ projectId={mockAction.projectId}
207
+ dataset={mockAction.dataset}
208
+ fallback={null}
209
+ >
210
+ {children}
211
+ </ResourceProvider>
212
+ ),
213
+ })
214
+ }).toThrow(/Mismatched resources found in actions/)
215
+ })
216
+
217
+ it('should throw an error when mixing projectId and resource (projectId first)', () => {
218
+ const actions = [
219
+ mockAction,
220
+ {
221
+ type: 'document.publish' as const,
222
+ documentId: 'doc2',
223
+ documentType: 'article',
224
+ resource: {projectId: 'p', dataset: 'd'},
225
+ },
226
+ ]
227
+
228
+ expect(() => {
229
+ renderHook(() => useDocumentPermissions(actions), {
230
+ wrapper: ({children}) => (
231
+ <ResourceProvider
232
+ projectId={mockAction.projectId}
233
+ dataset={mockAction.dataset}
234
+ fallback={null}
235
+ >
236
+ {children}
237
+ </ResourceProvider>
238
+ ),
239
+ })
240
+ }).toThrow(/Mismatches between projectId\/dataset options and resource/)
241
+ })
242
+
243
+ it('should throw an error when mixing resource and projectId (resource first)', () => {
244
+ const actions = [
245
+ {
246
+ type: 'document.publish' as const,
247
+ documentId: 'doc1',
248
+ documentType: 'article',
249
+ resource: {projectId: 'p', dataset: 'd'},
250
+ },
251
+ mockAction,
252
+ ]
253
+
254
+ expect(() => {
255
+ renderHook(() => useDocumentPermissions(actions), {
256
+ wrapper: ({children}) => (
257
+ <ResourceProvider
258
+ projectId={mockAction.projectId}
259
+ dataset={mockAction.dataset}
260
+ fallback={null}
261
+ >
262
+ {children}
263
+ </ResourceProvider>
264
+ ),
265
+ })
266
+ }).toThrow(/Mismatches between projectId\/dataset options and resource/)
267
+ })
268
+
185
269
  it('should wait for permissions to be ready before rendering', async () => {
186
270
  // Set up initial value as undefined (not ready)
187
271
  act(() => {
@@ -3,6 +3,7 @@ import {useCallback, useMemo, useSyncExternalStore} from 'react'
3
3
  import {filter, firstValueFrom} from 'rxjs'
4
4
 
5
5
  import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
6
7
 
7
8
  /**
8
9
  *
@@ -91,9 +92,15 @@ export function useDocumentPermissions(
91
92
  // if actions is an array, we need to check that all actions belong to the same project and dataset
92
93
  let projectId
93
94
  let dataset
95
+ let resource
94
96
 
95
97
  for (const action of actions) {
96
98
  if (action.projectId) {
99
+ if (resource) {
100
+ throw new Error(
101
+ `Mismatches between projectId/dataset options and resource in actions. Found projectId "${action.projectId}" and dataset "${action.dataset}" but expected resource "${resource}".`,
102
+ )
103
+ }
97
104
  if (!projectId) projectId = action.projectId
98
105
  if (action.projectId !== projectId) {
99
106
  throw new Error(
@@ -110,9 +117,24 @@ export function useDocumentPermissions(
110
117
  }
111
118
  }
112
119
  }
120
+
121
+ if (action.resource) {
122
+ if (!resource) resource = action.resource
123
+ if (action.resource !== resource) {
124
+ throw new Error(
125
+ `Mismatched resources found in actions. All actions must belong to the same resource. Found "${action.resource}" but expected "${resource}".`,
126
+ )
127
+ }
128
+ if (projectId || dataset) {
129
+ throw new Error(
130
+ `Mismatches between projectId/dataset options and resource in actions. Found "${action.resource}" but expected project "${projectId}" and dataset "${dataset}".`,
131
+ )
132
+ }
133
+ }
113
134
  }
114
135
 
115
136
  const instance = useSanityInstance({projectId, dataset})
137
+ trackHookUsage(instance, 'useDocumentPermissions')
116
138
  const isDocumentReady = useCallback(
117
139
  () => getPermissionsState(instance, {actions}).getCurrent() !== undefined,
118
140
  [actions, instance],
@@ -1,15 +1,25 @@
1
1
  import {getDocumentSyncStatus} from '@sanity/sdk'
2
2
  import {describe, it} from 'vitest'
3
3
 
4
+ import {renderHook} from '../../../test/test-utils'
4
5
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
6
 
6
7
  const mockHook = vi.fn()
7
8
  vi.mock('../helpers/createStateSourceHook', () => ({createStateSourceHook: vi.fn(() => mockHook)}))
8
- vi.mock('@sanity/sdk', () => ({getDocumentSyncStatus: vi.fn()}))
9
+ vi.mock('@sanity/sdk', async (importOriginal) => {
10
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
11
+ return {
12
+ ...original,
13
+ getDocumentSyncStatus: vi.fn(),
14
+ }
15
+ })
16
+
17
+ vi.mock('../context/useSanityInstance')
9
18
 
10
19
  describe('useDocumentSyncStatus', () => {
11
20
  it('calls `createStateSourceHook` with `getDocumentSyncStatus`', async () => {
12
21
  const {useDocumentSyncStatus} = await import('./useDocumentSyncStatus')
22
+ renderHook(() => useDocumentSyncStatus({documentId: '1', documentType: 'test'}))
13
23
  expect(createStateSourceHook).toHaveBeenCalledWith(
14
24
  expect.objectContaining({
15
25
  getState: getDocumentSyncStatus,
@@ -18,6 +28,7 @@ describe('useDocumentSyncStatus', () => {
18
28
  getConfig: expect.any(Function),
19
29
  }),
20
30
  )
21
- expect(useDocumentSyncStatus).toBe(mockHook)
31
+ // Verify that the hook was created and can be called
32
+ expect(mockHook).toHaveBeenCalled()
22
33
  })
23
34
  })
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  type DocumentHandle,
3
+ type DocumentOptions,
3
4
  getDocumentSyncStatus,
4
5
  resolveDocument,
5
6
  type SanityInstance,
@@ -8,6 +9,7 @@ import {
8
9
  import {identity} from 'rxjs'
9
10
 
10
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
11
13
 
12
14
  type UseDocumentSyncStatus = {
13
15
  /**
@@ -45,11 +47,7 @@ type UseDocumentSyncStatus = {
45
47
  (doc: DocumentHandle): boolean
46
48
  }
47
49
 
48
- /**
49
- * @public
50
- * @function
51
- */
52
- export const useDocumentSyncStatus: UseDocumentSyncStatus = createStateSourceHook({
50
+ const useDocumentSyncStatusValue = createStateSourceHook({
53
51
  getState: getDocumentSyncStatus as (
54
52
  instance: SanityInstance,
55
53
  doc: DocumentHandle,
@@ -59,3 +57,14 @@ export const useDocumentSyncStatus: UseDocumentSyncStatus = createStateSourceHoo
59
57
  suspender: (instance, doc: DocumentHandle) => resolveDocument(instance, doc),
60
58
  getConfig: identity,
61
59
  })
60
+
61
+ /**
62
+ * @public
63
+ * @function
64
+ */
65
+ export const useDocumentSyncStatus: UseDocumentSyncStatus = (
66
+ options: DocumentOptions<string | undefined>,
67
+ ) => {
68
+ const normalizedOptions = useNormalizedResourceOptions(options)
69
+ return useDocumentSyncStatusValue(normalizedOptions)
70
+ }
@@ -10,6 +10,8 @@ import {type SanityDocument} from 'groq'
10
10
  import {useCallback} from 'react'
11
11
 
12
12
  import {useSanityInstance} from '../context/useSanityInstance'
13
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
14
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
13
15
  import {useApplyDocumentActions} from './useApplyDocumentActions'
14
16
 
15
17
  const ignoredKeys = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']
@@ -258,24 +260,48 @@ export function useEditDocument<TData>(
258
260
  * }
259
261
  *
260
262
  * ```
263
+ *
264
+ * @example Edit a document in a release
265
+ * ```tsx
266
+ * import {useEditDocument} from '@sanity/sdk-react'
267
+ *
268
+ * function EditArticleInRelease({documentId}: {documentId: string}) {
269
+ * // Use the document's plain ID — not `versions.<releaseName>.<id>`.
270
+ * // The document must already exist in the release (added via `createDocument` first).
271
+ * const editArticle = useEditDocument({
272
+ * documentId,
273
+ * documentType: 'article',
274
+ * perspective: {releaseName: 'summer-drop'},
275
+ * })
276
+ *
277
+ * return (
278
+ * <button onClick={() => editArticle(prev => ({...prev, title: 'Updated for release'}))}>
279
+ * Edit in Release
280
+ * </button>
281
+ * )
282
+ * }
283
+ * ```
261
284
  */
262
285
  export function useEditDocument({
263
286
  path,
264
287
  ...doc
265
288
  }: DocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
266
289
  const instance = useSanityInstance(doc)
290
+ trackHookUsage(instance, 'useEditDocument')
291
+ const normalizedDoc = useNormalizedResourceOptions(doc)
292
+
267
293
  const apply = useApplyDocumentActions()
268
294
  const isDocumentReady = useCallback(
269
- () => getDocumentState(instance, doc).getCurrent() !== undefined,
270
- [instance, doc],
295
+ () => getDocumentState(instance, normalizedDoc).getCurrent() !== undefined,
296
+ [instance, normalizedDoc],
271
297
  )
272
- if (!isDocumentReady()) throw resolveDocument(instance, doc)
298
+ if (!isDocumentReady()) throw resolveDocument(instance, normalizedDoc)
273
299
 
274
300
  return (updater: Updater<unknown>) => {
275
301
  const currentPath = path
276
302
 
277
303
  if (currentPath) {
278
- const stateWithOptions = getDocumentState(instance, {...doc, path})
304
+ const stateWithOptions = getDocumentState(instance, {...normalizedDoc, path})
279
305
  const currentValue = stateWithOptions.getCurrent()
280
306
 
281
307
  const nextValue =
@@ -283,10 +309,10 @@ export function useEditDocument({
283
309
  ? (updater as (prev: typeof currentValue) => typeof currentValue)(currentValue)
284
310
  : updater
285
311
 
286
- return apply(editDocument(doc, {set: {[currentPath]: nextValue}}))
312
+ return apply(editDocument(normalizedDoc, {set: {[currentPath]: nextValue}}))
287
313
  }
288
314
 
289
- const fullDocState = getDocumentState(instance, {...doc, path})
315
+ const fullDocState = getDocumentState(instance, {...normalizedDoc, path})
290
316
  const current = fullDocState.getCurrent() as object | null | undefined
291
317
  const nextValue =
292
318
  typeof updater === 'function'
@@ -308,8 +334,8 @@ export function useEditDocument({
308
334
  )
309
335
  .map((key) =>
310
336
  key in nextValue
311
- ? editDocument(doc, {set: {[key]: (nextValue as Record<string, unknown>)[key]}})
312
- : editDocument(doc, {unset: [key]}),
337
+ ? editDocument(normalizedDoc, {set: {[key]: (nextValue as Record<string, unknown>)[key]}})
338
+ : editDocument(normalizedDoc, {unset: [key]}),
313
339
  )
314
340
 
315
341
  return apply(editActions)
@@ -5,10 +5,10 @@ import {
5
5
  type QueryOptions,
6
6
  } from '@sanity/sdk'
7
7
  import {type SortOrderingItem} from '@sanity/types'
8
- import {pick} from 'lodash-es'
9
- import {useCallback, useEffect, useMemo, useState} from 'react'
8
+ import {useCallback, useMemo, useState} from 'react'
10
9
 
11
10
  import {useSanityInstance} from '../context/useSanityInstance'
11
+ import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
12
12
  import {useQuery} from '../query/useQuery'
13
13
 
14
14
  const DEFAULT_BATCH_SIZE = 25
@@ -207,6 +207,7 @@ export function useDocuments<
207
207
  TDataset,
208
208
  TProjectId
209
209
  > {
210
+ useTrackHookUsage('useDocuments')
210
211
  const instance = useSanityInstance(options)
211
212
  const [limit, setLimit] = useState(batchSize)
212
213
  const documentTypes = useMemo(
@@ -228,9 +229,11 @@ export function useDocuments<
228
229
  types: documentTypes,
229
230
  ...options,
230
231
  })
231
- useEffect(() => {
232
+ const [prevKey, setPrevKey] = useState(key)
233
+ if (prevKey !== key) {
234
+ setPrevKey(key)
232
235
  setLimit(batchSize)
233
- }, [key, batchSize])
236
+ }
234
237
 
235
238
  const filterClause = useMemo(() => {
236
239
  const conditions: string[] = []
@@ -279,9 +282,11 @@ export function useDocuments<
279
282
  query: `{"count":${countQuery},"data":${dataQuery}}`,
280
283
  params: {
281
284
  ...params,
285
+ // these are passed back to the user as part of each document handle
282
286
  __handle: {
283
- ...pick(instance.config, 'projectId', 'dataset', 'perspective'),
284
- ...pick(options, 'projectId', 'dataset', 'perspective'),
287
+ projectId: options.projectId ?? instance.config.projectId,
288
+ dataset: options.dataset ?? instance.config.dataset,
289
+ perspective: options.perspective ?? instance.config.perspective,
285
290
  },
286
291
  __types: documentTypes,
287
292
  },