@sanity/sdk-react 2.7.0 → 2.9.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 (38) hide show
  1. package/dist/index.d.ts +144 -25
  2. package/dist/index.js +248 -139
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -5
  5. package/src/_exports/sdk-react.ts +1 -0
  6. package/src/components/SanityApp.tsx +1 -0
  7. package/src/components/auth/AuthBoundary.test.tsx +3 -0
  8. package/src/components/auth/LoginError.test.tsx +5 -0
  9. package/src/components/auth/LoginError.tsx +22 -1
  10. package/src/context/ResourceProvider.test.tsx +7 -1
  11. package/src/context/ResourceProvider.tsx +6 -0
  12. package/src/context/SDKStudioContext.ts +6 -0
  13. package/src/hooks/dashboard/useDispatchIntent.test.ts +2 -0
  14. package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
  15. package/src/hooks/dashboard/useWindowTitle.ts +112 -0
  16. package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
  17. package/src/hooks/document/useApplyDocumentActions.ts +99 -3
  18. package/src/hooks/document/useDocument.ts +22 -6
  19. package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
  20. package/src/hooks/document/useDocumentEvent.ts +10 -3
  21. package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
  22. package/src/hooks/document/useDocumentPermissions.ts +22 -0
  23. package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
  24. package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
  25. package/src/hooks/document/useEditDocument.ts +34 -8
  26. package/src/hooks/documents/useDocuments.ts +2 -0
  27. package/src/hooks/helpers/useNormalizedSourceOptions.ts +50 -28
  28. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  29. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +2 -0
  30. package/src/hooks/presence/usePresence.ts +2 -0
  31. package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
  32. package/src/hooks/preview/useDocumentPreview.tsx +39 -55
  33. package/src/hooks/projection/useDocumentProjection.ts +2 -0
  34. package/src/hooks/query/useQuery.ts +2 -0
  35. package/src/hooks/releases/useActiveReleases.ts +32 -13
  36. package/src/hooks/releases/usePerspective.ts +26 -14
  37. package/src/hooks/users/useUser.ts +2 -0
  38. package/src/hooks/users/useUsers.ts +2 -0
@@ -1,6 +1,7 @@
1
1
  import {applyDocumentActions, type SanityInstance} from '@sanity/sdk'
2
2
  import {describe, it} from 'vitest'
3
3
 
4
+ import {renderHook} from '../../../test/test-utils'
4
5
  import {useSanityInstance} from '../context/useSanityInstance'
5
6
  import {useApplyDocumentActions} from './useApplyDocumentActions'
6
7
 
@@ -31,8 +32,8 @@ describe('useApplyDocumentActions', () => {
31
32
  })
32
33
 
33
34
  it('uses the SanityInstance', async () => {
34
- const apply = useApplyDocumentActions()
35
- apply({
35
+ const {result} = renderHook(() => useApplyDocumentActions())
36
+ result.current({
36
37
  type: 'document.edit',
37
38
  documentType: 'post',
38
39
  documentId: 'abc',
@@ -50,8 +51,8 @@ describe('useApplyDocumentActions', () => {
50
51
  })
51
52
 
52
53
  it('uses SanityInstance.match when projectId is overrideen', async () => {
53
- const apply = useApplyDocumentActions()
54
- apply({
54
+ const {result} = renderHook(() => useApplyDocumentActions())
55
+ result.current({
55
56
  type: 'document.edit',
56
57
  documentType: 'post',
57
58
  documentId: 'abc',
@@ -73,8 +74,8 @@ describe('useApplyDocumentActions', () => {
73
74
  })
74
75
 
75
76
  it('uses SanityInstance when dataset is overrideen', async () => {
76
- const apply = useApplyDocumentActions()
77
- apply({
77
+ const {result} = renderHook(() => useApplyDocumentActions())
78
+ result.current({
78
79
  type: 'document.edit',
79
80
  documentType: 'post',
80
81
  documentId: 'abc',
@@ -96,8 +97,8 @@ describe('useApplyDocumentActions', () => {
96
97
  })
97
98
 
98
99
  it('uses SanityInstance.amcth when projectId and dataset is overrideen', async () => {
99
- const apply = useApplyDocumentActions()
100
- apply({
100
+ const {result} = renderHook(() => useApplyDocumentActions())
101
+ result.current({
101
102
  type: 'document.edit',
102
103
  documentType: 'post',
103
104
  documentId: 'abc',
@@ -121,9 +122,9 @@ describe('useApplyDocumentActions', () => {
121
122
  })
122
123
 
123
124
  it("throws if SanityInstance.match doesn't find anything", async () => {
124
- const apply = useApplyDocumentActions()
125
+ const {result} = renderHook(() => useApplyDocumentActions())
125
126
  expect(() => {
126
- apply({
127
+ result.current({
127
128
  type: 'document.edit',
128
129
  documentType: 'post',
129
130
  documentId: 'abc',
@@ -132,4 +133,106 @@ describe('useApplyDocumentActions', () => {
132
133
  })
133
134
  }).toThrow()
134
135
  })
136
+
137
+ it('throws when actions have mismatched project IDs', async () => {
138
+ const {result} = renderHook(() => useApplyDocumentActions())
139
+ expect(() => {
140
+ result.current([
141
+ {
142
+ type: 'document.edit',
143
+ documentType: 'post',
144
+ documentId: 'abc',
145
+ projectId: 'p123',
146
+ },
147
+ {
148
+ type: 'document.edit',
149
+ documentType: 'post',
150
+ documentId: 'def',
151
+ projectId: 'p456',
152
+ },
153
+ ])
154
+ }).toThrow(/Mismatched project IDs found in actions/)
155
+ })
156
+
157
+ it('throws when actions have mismatched datasets', async () => {
158
+ const {result} = renderHook(() => useApplyDocumentActions())
159
+ expect(() => {
160
+ result.current([
161
+ {
162
+ type: 'document.edit',
163
+ documentType: 'post',
164
+ documentId: 'abc',
165
+ projectId: 'p',
166
+ dataset: 'd1',
167
+ },
168
+ {
169
+ type: 'document.edit',
170
+ documentType: 'post',
171
+ documentId: 'def',
172
+ projectId: 'p',
173
+ dataset: 'd2',
174
+ },
175
+ ])
176
+ }).toThrow(/Mismatched datasets found in actions/)
177
+ })
178
+
179
+ it('throws when actions have mismatched sources', async () => {
180
+ const {result} = renderHook(() => useApplyDocumentActions())
181
+ expect(() => {
182
+ result.current([
183
+ {
184
+ type: 'document.edit',
185
+ documentType: 'post',
186
+ documentId: 'abc',
187
+ source: {projectId: 'p', dataset: 'd1'},
188
+ },
189
+ {
190
+ type: 'document.edit',
191
+ documentType: 'post',
192
+ documentId: 'def',
193
+ source: {projectId: 'p', dataset: 'd2'},
194
+ },
195
+ ])
196
+ }).toThrow(/Mismatched sources found in actions/)
197
+ })
198
+
199
+ it('throws when mixing projectId and source (projectId first)', async () => {
200
+ const {result} = renderHook(() => useApplyDocumentActions())
201
+ expect(() => {
202
+ result.current([
203
+ {
204
+ type: 'document.edit',
205
+ documentType: 'post',
206
+ documentId: 'abc',
207
+ projectId: 'p',
208
+ },
209
+ {
210
+ type: 'document.edit',
211
+ documentType: 'post',
212
+ documentId: 'def',
213
+ source: {projectId: 'p', dataset: 'd'},
214
+ },
215
+ ])
216
+ }).toThrow(/Mismatches between projectId\/dataset options and source/)
217
+ })
218
+
219
+ it('throws when mixing source and projectId (source first)', async () => {
220
+ const {result} = renderHook(() => useApplyDocumentActions())
221
+ expect(() => {
222
+ result.current([
223
+ {
224
+ type: 'document.edit',
225
+ documentType: 'post',
226
+ documentId: 'abc',
227
+ source: {projectId: 'p', dataset: 'd'},
228
+ },
229
+ {
230
+ type: 'document.edit',
231
+ documentType: 'post',
232
+ documentId: 'def',
233
+ projectId: 'p',
234
+ },
235
+ ])
236
+ }).toThrow(/Mismatches between projectId\/dataset options and source/)
237
+ })
135
238
  })
@@ -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 {SourcesContext} from '../../context/SourcesContext'
9
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
+ import {
13
+ normalizeSourceOptions,
14
+ type WithSourceNameSupport,
15
+ } from '../helpers/useNormalizedSourceOptions'
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?: WithSourceNameSupport<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 sources = useContext(SourcesContext)
155
222
 
156
223
  return (actionOrActions, options) => {
157
224
  const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
225
+ const normalizedOptions = options ? normalizeSourceOptions(options, sources) : undefined
158
226
 
159
227
  let projectId
160
228
  let dataset
229
+ let source
161
230
  for (const action of actions) {
162
231
  if (action.projectId) {
232
+ if (source) {
233
+ throw new Error(
234
+ `Mismatches between projectId/dataset options and source in actions. Found projectId "${action.projectId}" and dataset "${action.dataset}" but expected source "${source}".`,
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.source) {
255
+ if (!source) source = action.source
256
+ if (action.source !== source) {
257
+ throw new Error(
258
+ `Mismatched sources found in actions. All actions must belong to the same source. Found "${action.source}" but expected "${source}".`,
259
+ )
260
+ }
261
+ if (projectId || dataset) {
262
+ throw new Error(
263
+ `Mismatches between projectId/dataset options and source in actions. Found "${action.source}" 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
+ source,
281
+ ...normalizedOptions,
282
+ })
191
283
  }
192
284
 
193
- return applyDocumentActions(instance, {actions, ...options})
285
+ return applyDocumentActions(instance, {
286
+ actions,
287
+ source,
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
+ useNormalizedSourceOptions,
8
+ type WithSourceNameSupport,
9
+ } from '../helpers/useNormalizedSourceOptions'
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
+ > = WithSourceNameSupport<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 = useNormalizedSourceOptions(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 {useNormalizedSourceOptions} from '../helpers/useNormalizedSourceOptions'
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 = useNormalizedSourceOptions(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
+ source: datasetHandle.source,
99
+ })
100
+ }, [instance, datasetHandle.source, 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 sources', () => {
187
+ const actions = [
188
+ {
189
+ type: 'document.publish' as const,
190
+ documentId: 'doc1',
191
+ documentType: 'article',
192
+ source: {projectId: 'p1', dataset: 'd1'},
193
+ },
194
+ {
195
+ type: 'document.publish' as const,
196
+ documentId: 'doc2',
197
+ documentType: 'article',
198
+ source: {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 sources found in actions/)
215
+ })
216
+
217
+ it('should throw an error when mixing projectId and source (projectId first)', () => {
218
+ const actions = [
219
+ mockAction,
220
+ {
221
+ type: 'document.publish' as const,
222
+ documentId: 'doc2',
223
+ documentType: 'article',
224
+ source: {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 source/)
241
+ })
242
+
243
+ it('should throw an error when mixing source and projectId (source first)', () => {
244
+ const actions = [
245
+ {
246
+ type: 'document.publish' as const,
247
+ documentId: 'doc1',
248
+ documentType: 'article',
249
+ source: {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 source/)
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 source
94
96
 
95
97
  for (const action of actions) {
96
98
  if (action.projectId) {
99
+ if (source) {
100
+ throw new Error(
101
+ `Mismatches between projectId/dataset options and source in actions. Found projectId "${action.projectId}" and dataset "${action.dataset}" but expected source "${source}".`,
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.source) {
122
+ if (!source) source = action.source
123
+ if (action.source !== source) {
124
+ throw new Error(
125
+ `Mismatched sources found in actions. All actions must belong to the same source. Found "${action.source}" but expected "${source}".`,
126
+ )
127
+ }
128
+ if (projectId || dataset) {
129
+ throw new Error(
130
+ `Mismatches between projectId/dataset options and source in actions. Found "${action.source}" 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 {useNormalizedSourceOptions} from '../helpers/useNormalizedSourceOptions'
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 = useNormalizedSourceOptions(options)
69
+ return useDocumentSyncStatusValue(normalizedOptions)
70
+ }