@sanity/sdk-react 2.9.0 → 2.11.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 (72) hide show
  1. package/dist/index.d.ts +338 -215
  2. package/dist/index.js +564 -342
  3. package/dist/index.js.map +1 -1
  4. package/package.json +9 -14
  5. package/src/_exports/index.ts +2 -0
  6. package/src/_exports/sdk-react.ts +8 -0
  7. package/src/components/SDKProvider.test.tsx +5 -12
  8. package/src/components/SDKProvider.tsx +58 -28
  9. package/src/components/SanityApp.tsx +2 -2
  10. package/src/components/auth/AuthBoundary.tsx +8 -1
  11. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  12. package/src/components/auth/LoginError.test.tsx +191 -5
  13. package/src/components/auth/LoginError.tsx +100 -56
  14. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  15. package/src/components/errors/ChunkLoadError.tsx +56 -0
  16. package/src/components/errors/chunkReloadStorage.ts +57 -0
  17. package/src/config/handles.ts +55 -0
  18. package/src/constants.ts +5 -0
  19. package/src/context/DefaultResourceContext.ts +10 -0
  20. package/src/context/PerspectiveContext.ts +12 -0
  21. package/src/context/ResourceProvider.test.tsx +2 -2
  22. package/src/context/ResourceProvider.tsx +56 -51
  23. package/src/context/ResourcesContext.tsx +7 -0
  24. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  25. package/src/context/SanityInstanceProvider.tsx +71 -0
  26. package/src/hooks/agent/agentActions.ts +55 -38
  27. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  28. package/src/hooks/context/useResource.test.tsx +32 -0
  29. package/src/hooks/context/useResource.ts +24 -0
  30. package/src/hooks/context/useSanityInstance.test.tsx +42 -111
  31. package/src/hooks/context/useSanityInstance.ts +28 -50
  32. package/src/hooks/dashboard/useDispatchIntent.test.ts +11 -7
  33. package/src/hooks/dashboard/useDispatchIntent.ts +7 -7
  34. package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
  35. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
  36. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -17
  37. package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +46 -81
  38. package/src/hooks/document/useApplyDocumentActions.ts +33 -67
  39. package/src/hooks/document/useDocument.ts +4 -6
  40. package/src/hooks/document/useDocumentEvent.ts +8 -7
  41. package/src/hooks/document/useDocumentPermissions.test.tsx +60 -152
  42. package/src/hooks/document/useDocumentPermissions.ts +78 -55
  43. package/src/hooks/document/useDocumentSyncStatus.ts +2 -2
  44. package/src/hooks/document/useEditDocument.test.tsx +25 -60
  45. package/src/hooks/document/useEditDocument.ts +3 -3
  46. package/src/hooks/documents/useDocuments.ts +19 -11
  47. package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
  48. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
  49. package/src/hooks/helpers/useNormalizedResourceOptions.ts +169 -0
  50. package/src/hooks/helpers/useTrackHookUsage.ts +2 -2
  51. package/src/hooks/organizations/useOrganization.test-d.ts +53 -0
  52. package/src/hooks/organizations/useOrganization.test.ts +65 -0
  53. package/src/hooks/organizations/useOrganization.ts +40 -0
  54. package/src/hooks/organizations/useOrganizations.test-d.ts +55 -0
  55. package/src/hooks/organizations/useOrganizations.test.ts +85 -0
  56. package/src/hooks/organizations/useOrganizations.ts +45 -0
  57. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +29 -14
  58. package/src/hooks/presence/usePresence.test.tsx +56 -9
  59. package/src/hooks/presence/usePresence.ts +16 -4
  60. package/src/hooks/preview/useDocumentPreview.tsx +8 -10
  61. package/src/hooks/projection/useDocumentProjection.ts +7 -9
  62. package/src/hooks/projects/useProject.test-d.ts +49 -0
  63. package/src/hooks/projects/useProject.ts +33 -41
  64. package/src/hooks/projects/useProjects.test-d.ts +49 -0
  65. package/src/hooks/projects/useProjects.ts +17 -23
  66. package/src/hooks/query/useQuery.ts +11 -10
  67. package/src/hooks/releases/useActiveReleases.ts +14 -14
  68. package/src/hooks/releases/usePerspective.ts +11 -16
  69. package/src/hooks/users/useUser.ts +1 -1
  70. package/src/hooks/users/useUsers.ts +1 -1
  71. package/src/context/SourcesContext.tsx +0 -7
  72. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -107
@@ -1,8 +1,9 @@
1
1
  import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
2
- import {act, renderHook, waitFor} from '@testing-library/react'
2
+ import {renderHook as reactRenderHook} from '@testing-library/react'
3
3
  import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs'
4
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
4
+ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
5
5
 
6
+ import {act, renderHook, waitFor} from '../../../test/test-utils'
6
7
  import {ResourceProvider} from '../../context/ResourceProvider'
7
8
  import {useDocumentPermissions} from './useDocumentPermissions'
8
9
 
@@ -24,12 +25,12 @@ vi.mock('rxjs', async (importOriginal) => {
24
25
  })
25
26
 
26
27
  describe('usePermissions', () => {
28
+ const mockResource = {projectId: 'project1', dataset: 'dataset1'}
27
29
  const mockAction: DocumentAction = {
28
30
  type: 'document.publish',
29
31
  documentId: 'doc1',
30
32
  documentType: 'article',
31
- projectId: 'project1',
32
- dataset: 'dataset1',
33
+ resource: mockResource,
33
34
  }
34
35
 
35
36
  const mockPermissionAllowed: DocumentPermissionsResult = {allowed: true}
@@ -46,8 +47,8 @@ describe('usePermissions', () => {
46
47
  }
47
48
 
48
49
  let permissionsSubject: BehaviorSubject<DocumentPermissionsResult | undefined>
49
- let mockSubscribe: ReturnType<typeof vi.fn>
50
- let mockGetCurrent: ReturnType<typeof vi.fn>
50
+ let mockSubscribe: Mock<(onStoreChanged?: () => void) => () => void>
51
+ let mockGetCurrent: Mock<() => DocumentPermissionsResult | undefined>
51
52
 
52
53
  beforeEach(() => {
53
54
  vi.clearAllMocks()
@@ -71,7 +72,7 @@ describe('usePermissions', () => {
71
72
  observable:
72
73
  permissionsSubject.asObservable() as unknown as Observable<DocumentPermissionsResult>,
73
74
  subscribe: mockSubscribe,
74
- getCurrent: mockGetCurrent,
75
+ getCurrent: mockGetCurrent as unknown as () => DocumentPermissionsResult,
75
76
  })
76
77
  })
77
78
 
@@ -85,20 +86,13 @@ describe('usePermissions', () => {
85
86
  permissionsSubject.next(mockPermissionAllowed)
86
87
  })
87
88
 
88
- const {result} = renderHook(() => useDocumentPermissions(mockAction), {
89
- wrapper: ({children}) => (
90
- <ResourceProvider
91
- projectId={mockAction.projectId}
92
- dataset={mockAction.dataset}
93
- fallback={null}
94
- >
95
- {children}
96
- </ResourceProvider>
97
- ),
98
- })
89
+ const {result} = renderHook(() => useDocumentPermissions(mockAction))
99
90
 
100
91
  // ResourceProvider handles the instance configuration
101
- expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), {actions: [mockAction]})
92
+ expect(getPermissionsState).toHaveBeenCalledWith(
93
+ expect.any(Object),
94
+ expect.objectContaining({actions: [mockAction], resource: mockResource}),
95
+ )
102
96
  expect(result.current).toEqual(mockPermissionAllowed)
103
97
  })
104
98
 
@@ -108,17 +102,7 @@ describe('usePermissions', () => {
108
102
  permissionsSubject.next(mockPermissionDenied)
109
103
  })
110
104
 
111
- const {result} = renderHook(() => useDocumentPermissions(mockAction), {
112
- wrapper: ({children}) => (
113
- <ResourceProvider
114
- projectId={mockAction.projectId}
115
- dataset={mockAction.dataset}
116
- fallback={null}
117
- >
118
- {children}
119
- </ResourceProvider>
120
- ),
121
- })
105
+ const {result} = renderHook(() => useDocumentPermissions(mockAction))
122
106
 
123
107
  expect(result.current).toEqual(mockPermissionDenied)
124
108
  expect(result.current.allowed).toBe(false)
@@ -129,141 +113,58 @@ describe('usePermissions', () => {
129
113
  it('should accept an array of actions', () => {
130
114
  const actions = [mockAction, {...mockAction, documentId: 'doc2'}]
131
115
 
132
- renderHook(() => useDocumentPermissions(actions), {
133
- wrapper: ({children}) => (
134
- <ResourceProvider
135
- projectId={mockAction.projectId}
136
- dataset={mockAction.dataset}
137
- fallback={null}
138
- >
139
- {children}
140
- </ResourceProvider>
141
- ),
142
- })
116
+ renderHook(() => useDocumentPermissions(actions))
143
117
 
144
- expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), {actions})
118
+ expect(getPermissionsState).toHaveBeenCalledWith(
119
+ expect.any(Object),
120
+ expect.objectContaining({actions, resource: mockResource}),
121
+ )
145
122
  })
146
123
 
147
- it('should throw an error if actions have mismatched project IDs', () => {
124
+ it('should throw an error if actions have mismatched resources', () => {
148
125
  const actions = [
149
126
  mockAction,
150
- {...mockAction, projectId: 'different-project', documentId: 'doc2'},
151
- ]
152
-
153
- expect(() => {
154
- renderHook(() => useDocumentPermissions(actions), {
155
- wrapper: ({children}) => (
156
- <ResourceProvider
157
- projectId={mockAction.projectId}
158
- dataset={mockAction.dataset}
159
- fallback={null}
160
- >
161
- {children}
162
- </ResourceProvider>
163
- ),
164
- })
165
- }).toThrow(/Mismatched project IDs found in actions/)
166
- })
167
-
168
- it('should throw an error if actions have mismatched datasets', () => {
169
- const actions = [mockAction, {...mockAction, dataset: 'different-dataset', documentId: 'doc2'}]
170
-
171
- expect(() => {
172
- renderHook(() => useDocumentPermissions(actions), {
173
- wrapper: ({children}) => (
174
- <ResourceProvider
175
- projectId={mockAction.projectId}
176
- dataset={mockAction.dataset}
177
- fallback={null}
178
- >
179
- {children}
180
- </ResourceProvider>
181
- ),
182
- })
183
- }).toThrow(/Mismatched datasets found in actions/)
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
127
  {
195
- type: 'document.publish' as const,
128
+ ...mockAction,
129
+ resource: {projectId: 'different-project', dataset: 'dataset1'},
196
130
  documentId: 'doc2',
197
- documentType: 'article',
198
- source: {projectId: 'p2', dataset: 'd2'},
199
131
  },
200
132
  ]
201
133
 
202
134
  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/)
135
+ renderHook(() => useDocumentPermissions(actions))
136
+ }).toThrow(/Mismatched resources found in actions/)
215
137
  })
216
138
 
217
- it('should throw an error when mixing projectId and source (projectId first)', () => {
139
+ it('should throw an error if actions have mismatched datasets', () => {
218
140
  const actions = [
219
141
  mockAction,
220
142
  {
221
- type: 'document.publish' as const,
143
+ ...mockAction,
144
+ resource: {projectId: 'project1', dataset: 'different-dataset'},
222
145
  documentId: 'doc2',
223
- documentType: 'article',
224
- source: {projectId: 'p', dataset: 'd'},
225
146
  },
226
147
  ]
227
148
 
228
149
  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/)
150
+ renderHook(() => useDocumentPermissions(actions))
151
+ }).toThrow(/Mismatched resources found in actions/)
241
152
  })
242
153
 
243
- it('should throw an error when mixing source and projectId (source first)', () => {
154
+ it('should throw an error when mixing different resources', () => {
244
155
  const actions = [
156
+ mockAction,
245
157
  {
246
158
  type: 'document.publish' as const,
247
- documentId: 'doc1',
159
+ documentId: 'doc2',
248
160
  documentType: 'article',
249
- source: {projectId: 'p', dataset: 'd'},
161
+ resource: {projectId: 'p', dataset: 'd'},
250
162
  },
251
- mockAction,
252
163
  ]
253
164
 
254
165
  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/)
166
+ renderHook(() => useDocumentPermissions(actions))
167
+ }).toThrow(/Mismatched resources found in actions/)
267
168
  })
268
169
 
269
170
  it('should wait for permissions to be ready before rendering', async () => {
@@ -277,7 +178,7 @@ describe('usePermissions', () => {
277
178
  vi.mocked(firstValueFrom).mockReturnValueOnce(mockPromise)
278
179
 
279
180
  // This should throw the promise and suspend
280
- const {result} = renderHook(
181
+ const {result} = reactRenderHook(
281
182
  () => {
282
183
  try {
283
184
  return useDocumentPermissions(mockAction)
@@ -290,11 +191,7 @@ describe('usePermissions', () => {
290
191
  },
291
192
  {
292
193
  wrapper: ({children}) => (
293
- <ResourceProvider
294
- projectId={mockAction.projectId}
295
- dataset={mockAction.dataset}
296
- fallback={null}
297
- >
194
+ <ResourceProvider resource={mockResource} fallback={null}>
298
195
  {children}
299
196
  </ResourceProvider>
300
197
  ),
@@ -310,27 +207,38 @@ describe('usePermissions', () => {
310
207
 
311
208
  // Now it should render properly
312
209
  await waitFor(() => {
313
- expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), {actions: [mockAction]})
210
+ expect(getPermissionsState).toHaveBeenCalledWith(
211
+ expect.any(Object),
212
+ expect.objectContaining({actions: [mockAction], resource: mockResource}),
213
+ )
314
214
  })
315
215
  })
316
216
 
217
+ it('throws when no resource is found from action or context', () => {
218
+ // Provide SanityInstance via ResourceProvider but no resource, so contextResource is undefined
219
+ expect(() => {
220
+ reactRenderHook(
221
+ () =>
222
+ useDocumentPermissions({
223
+ type: 'document.publish',
224
+ documentId: 'doc1',
225
+ documentType: 'article',
226
+ // no resource
227
+ } as never),
228
+ {
229
+ wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
230
+ },
231
+ )
232
+ }).toThrow(/No resource found/)
233
+ })
234
+
317
235
  it('should react to permission state changes', async () => {
318
236
  // Start with permission allowed
319
237
  act(() => {
320
238
  permissionsSubject.next(mockPermissionAllowed)
321
239
  })
322
240
 
323
- const {result, rerender} = renderHook(() => useDocumentPermissions(mockAction), {
324
- wrapper: ({children}) => (
325
- <ResourceProvider
326
- projectId={mockAction.projectId}
327
- dataset={mockAction.dataset}
328
- fallback={null}
329
- >
330
- {children}
331
- </ResourceProvider>
332
- ),
333
- })
241
+ const {result, rerender} = renderHook(() => useDocumentPermissions(mockAction))
334
242
 
335
243
  expect(result.current).toEqual(mockPermissionAllowed)
336
244
 
@@ -1,10 +1,20 @@
1
1
  import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
2
- import {useCallback, useMemo, useSyncExternalStore} from 'react'
2
+ import {isDeepEqual} from '@sanity/sdk/_internal'
3
+ import {useCallback, useContext, useMemo, useSyncExternalStore} from 'react'
3
4
  import {filter, firstValueFrom} from 'rxjs'
4
5
 
6
+ import {ResourcesContext} from '../../context/ResourcesContext'
5
7
  import {useSanityInstance} from '../context/useSanityInstance'
8
+ import {
9
+ normalizeResourceOptions,
10
+ useEffectiveContextResource,
11
+ type WithResourceNameSupport,
12
+ } from '../helpers/useNormalizedResourceOptions'
6
13
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
7
14
 
15
+ const noopSubscribe = () => () => {}
16
+ const returnUndefined = () => undefined
17
+
8
18
  /**
9
19
  *
10
20
  * @public
@@ -83,74 +93,87 @@ import {trackHookUsage} from '../helpers/useTrackHookUsage'
83
93
  * ```
84
94
  */
85
95
  export function useDocumentPermissions(
86
- actionOrActions: DocumentAction | DocumentAction[],
96
+ actionOrActions:
97
+ | WithResourceNameSupport<DocumentAction>
98
+ | WithResourceNameSupport<DocumentAction>[],
87
99
  ): DocumentPermissionsResult {
88
- const actions = useMemo(
89
- () => (Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]),
90
- [actionOrActions],
91
- )
92
- // if actions is an array, we need to check that all actions belong to the same project and dataset
93
- let projectId
94
- let dataset
95
- let source
100
+ const instance = useSanityInstance()
101
+ trackHookUsage(instance, 'useDocumentPermissions')
102
+ const effectiveContextResource = useEffectiveContextResource()
103
+ const resources = useContext(ResourcesContext)
96
104
 
97
- for (const action of actions) {
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}".`,
105
+ const {
106
+ actions: normalizedActions,
107
+ resource: actionResource,
108
+ error: validationError,
109
+ } = useMemo(() => {
110
+ const normalized = Array.isArray(actionOrActions)
111
+ ? actionOrActions.map((action) =>
112
+ normalizeResourceOptions(action, resources, effectiveContextResource),
102
113
  )
103
- }
104
- if (!projectId) projectId = action.projectId
105
- if (action.projectId !== projectId) {
106
- throw new Error(
107
- `Mismatched project IDs found in actions. All actions must belong to the same project. Found "${action.projectId}" but expected "${projectId}".`,
108
- )
109
- }
114
+ : [normalizeResourceOptions(actionOrActions, resources, effectiveContextResource)]
110
115
 
111
- if (action.dataset) {
112
- if (!dataset) dataset = action.dataset
113
- if (action.dataset !== dataset) {
114
- throw new Error(
115
- `Mismatched datasets found in actions. All actions must belong to the same dataset. Found "${action.dataset}" but expected "${dataset}".`,
116
- )
116
+ let resource
117
+ for (const action of normalized) {
118
+ if (action.resource) {
119
+ if (!resource) resource = action.resource
120
+ if (!isDeepEqual(action.resource, resource)) {
121
+ return {
122
+ actions: normalized,
123
+ resource,
124
+ error: new Error(
125
+ `Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
126
+ ),
127
+ }
117
128
  }
118
129
  }
119
130
  }
131
+ return {actions: normalized, resource, error: undefined}
132
+ }, [actionOrActions, resources, effectiveContextResource])
120
133
 
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
- }
134
- }
134
+ const effectiveResource = actionResource ?? effectiveContextResource
135
+
136
+ // Keep hooks unconditional — validation errors and missing-resource errors are
137
+ // thrown after all hooks so that the hook call count stays stable across renders.
138
+ const permissionsOptions = useMemo(
139
+ () =>
140
+ effectiveResource
141
+ ? {
142
+ resource: effectiveResource,
143
+ // `Omit<>` on `DocumentAction` loses the discriminant; runtime values are still actions.
144
+ actions: normalizedActions as DocumentAction[],
145
+ }
146
+ : undefined,
147
+ [effectiveResource, normalizedActions],
148
+ )
149
+
150
+ const stateSource = useMemo(
151
+ () => (permissionsOptions ? getPermissionsState(instance, permissionsOptions) : undefined),
152
+ [permissionsOptions, instance],
153
+ )
135
154
 
136
- const instance = useSanityInstance({projectId, dataset})
137
- trackHookUsage(instance, 'useDocumentPermissions')
138
155
  const isDocumentReady = useCallback(
139
- () => getPermissionsState(instance, {actions}).getCurrent() !== undefined,
140
- [actions, instance],
156
+ () => stateSource !== undefined && stateSource.getCurrent() !== undefined,
157
+ [stateSource],
158
+ )
159
+
160
+ const result = useSyncExternalStore(
161
+ stateSource?.subscribe ?? noopSubscribe,
162
+ stateSource?.getCurrent ?? returnUndefined,
141
163
  )
164
+
165
+ // All hooks have been called — safe to throw now.
166
+ if (validationError) throw validationError
167
+ if (!effectiveResource) {
168
+ throw new Error(
169
+ 'No resource found. Provide a resource via the action handle or wrap with a resource context.',
170
+ )
171
+ }
142
172
  if (!isDocumentReady()) {
143
173
  throw firstValueFrom(
144
- getPermissionsState(instance, {actions}).observable.pipe(
145
- filter((result) => result !== undefined),
146
- ),
174
+ stateSource!.observable.pipe(filter((permissions) => permissions !== undefined)),
147
175
  )
148
176
  }
149
177
 
150
- const {subscribe, getCurrent} = useMemo(
151
- () => getPermissionsState(instance, {actions}),
152
- [actions, instance],
153
- )
154
-
155
- return useSyncExternalStore(subscribe, getCurrent) as DocumentPermissionsResult
178
+ return result as DocumentPermissionsResult
156
179
  }
@@ -9,7 +9,7 @@ import {
9
9
  import {identity} from 'rxjs'
10
10
 
11
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
- import {useNormalizedSourceOptions} from '../helpers/useNormalizedSourceOptions'
12
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
13
13
 
14
14
  type UseDocumentSyncStatus = {
15
15
  /**
@@ -65,6 +65,6 @@ const useDocumentSyncStatusValue = createStateSourceHook({
65
65
  export const useDocumentSyncStatus: UseDocumentSyncStatus = (
66
66
  options: DocumentOptions<string | undefined>,
67
67
  ) => {
68
- const normalizedOptions = useNormalizedSourceOptions(options)
68
+ const normalizedOptions = useNormalizedResourceOptions(options)
69
69
  return useDocumentSyncStatusValue(normalizedOptions)
70
70
  }
@@ -7,10 +7,9 @@ import {
7
7
  type StateSource,
8
8
  } from '@sanity/sdk'
9
9
  import {type SanityDocument} from '@sanity/types'
10
- import {renderHook} from '@testing-library/react'
11
10
  import {beforeEach, describe, expect, it, vi} from 'vitest'
12
11
 
13
- import {ResourceProvider} from '../../context/ResourceProvider'
12
+ import {renderHook} from '../../../test/test-utils'
14
13
  import {useApplyDocumentActions} from './useApplyDocumentActions'
15
14
  import {useEditDocument} from './useEditDocument'
16
15
 
@@ -42,6 +41,11 @@ const docHandle = createDocumentHandle({
42
41
  documentType: 'book',
43
42
  })
44
43
 
44
+ const normalizedDoc = {
45
+ ...docHandle,
46
+ resource: {projectId: 'test', dataset: 'test'},
47
+ }
48
+
45
49
  // Define a single generic TestDocument type
46
50
  interface Book extends SanityDocument {
47
51
  _type: 'book'
@@ -76,16 +80,10 @@ describe('useEditDocument hook', () => {
76
80
  const apply = vi.fn().mockResolvedValue({transactionId: 'tx1'})
77
81
  vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
78
82
 
79
- const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}), {
80
- wrapper: ({children}) => (
81
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
82
- {children}
83
- </ResourceProvider>
84
- ),
85
- })
83
+ const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
86
84
  const promise = result.current('newValue')
87
- expect(editDocument).toHaveBeenCalledWith(docHandle, {set: {foo: 'newValue'}})
88
- expect(apply).toHaveBeenCalledWith(editDocument(docHandle, {set: {foo: 'newValue'}}))
85
+ expect(editDocument).toHaveBeenCalledWith(normalizedDoc, {set: {foo: 'newValue'}})
86
+ expect(apply).toHaveBeenCalledWith(editDocument(normalizedDoc, {set: {foo: 'newValue'}}))
89
87
  const actionsResult = await promise
90
88
  expect(actionsResult).toEqual({transactionId: 'tx1'})
91
89
  })
@@ -103,15 +101,9 @@ describe('useEditDocument hook', () => {
103
101
  const apply = vi.fn().mockResolvedValue({transactionId: 'tx2'})
104
102
  vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
105
103
 
106
- const {result} = renderHook(() => useEditDocument(docHandle), {
107
- wrapper: ({children}) => (
108
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
109
- {children}
110
- </ResourceProvider>
111
- ),
112
- })
104
+ const {result} = renderHook(() => useEditDocument(docHandle))
113
105
  const promise = result.current({...doc, foo: 'baz', extra: 'old', _id: 'doc1'})
114
- expect(apply).toHaveBeenCalledWith([editDocument(docHandle, {set: {foo: 'baz'}})])
106
+ expect(apply).toHaveBeenCalledWith([editDocument(normalizedDoc, {set: {foo: 'baz'}})])
115
107
  const actionsResult = await promise
116
108
  expect(actionsResult).toEqual({transactionId: 'tx2'})
117
109
  })
@@ -127,16 +119,10 @@ describe('useEditDocument hook', () => {
127
119
  const apply = vi.fn().mockResolvedValue({transactionId: 'tx3'})
128
120
  vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
129
121
 
130
- const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}), {
131
- wrapper: ({children}) => (
132
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
133
- {children}
134
- </ResourceProvider>
135
- ),
136
- })
122
+ const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
137
123
  const promise = result.current((prev: unknown) => `${prev}Updated`) // 'bar' becomes 'barUpdated'
138
- expect(editDocument).toHaveBeenCalledWith(docHandle, {set: {foo: 'barUpdated'}})
139
- expect(apply).toHaveBeenCalledWith(editDocument(docHandle, {set: {foo: 'barUpdated'}}))
124
+ expect(editDocument).toHaveBeenCalledWith(normalizedDoc, {set: {foo: 'barUpdated'}})
125
+ expect(apply).toHaveBeenCalledWith(editDocument(normalizedDoc, {set: {foo: 'barUpdated'}}))
140
126
  const actionsResult = await promise
141
127
  expect(actionsResult).toEqual({transactionId: 'tx3'})
142
128
  })
@@ -153,15 +139,9 @@ describe('useEditDocument hook', () => {
153
139
  const apply = vi.fn().mockResolvedValue({transactionId: 'tx4'})
154
140
  vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
155
141
 
156
- const {result} = renderHook(() => useEditDocument(docHandle), {
157
- wrapper: ({children}) => (
158
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
159
- {children}
160
- </ResourceProvider>
161
- ),
162
- })
163
- const promise = result.current((prevDoc) => ({...prevDoc, foo: 'baz'}))
164
- expect(apply).toHaveBeenCalledWith([editDocument(docHandle, {set: {foo: 'baz'}})])
142
+ const {result} = renderHook(() => useEditDocument(docHandle))
143
+ const promise = result.current((prevDoc: Book) => ({...prevDoc, foo: 'baz'}))
144
+ expect(apply).toHaveBeenCalledWith([editDocument(normalizedDoc, {set: {foo: 'baz'}})])
165
145
  const actionsResult = await promise
166
146
  expect(actionsResult).toEqual({transactionId: 'tx4'})
167
147
  })
@@ -177,13 +157,7 @@ describe('useEditDocument hook', () => {
177
157
  const fakeApply = vi.fn()
178
158
  vi.mocked(useApplyDocumentActions).mockReturnValue(fakeApply)
179
159
 
180
- const {result} = renderHook(() => useEditDocument(docHandle), {
181
- wrapper: ({children}) => (
182
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
183
- {children}
184
- </ResourceProvider>
185
- ),
186
- })
160
+ const {result} = renderHook(() => useEditDocument(docHandle))
187
161
  expect(() => result.current('notAnObject' as unknown as Book)).toThrowError(
188
162
  'No path was provided to `useEditDocument` and the value provided was not a document object.',
189
163
  )
@@ -203,22 +177,13 @@ describe('useEditDocument hook', () => {
203
177
  vi.mocked(resolveDocument).mockReturnValue(resolveDocPromise)
204
178
 
205
179
  // Render the hook and capture the thrown promise.
206
- const {result} = renderHook(
207
- () => {
208
- try {
209
- return useEditDocument(docHandle)
210
- } catch (e) {
211
- return e
212
- }
213
- },
214
- {
215
- wrapper: ({children}) => (
216
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
217
- {children}
218
- </ResourceProvider>
219
- ),
220
- },
221
- )
180
+ const {result} = renderHook(() => {
181
+ try {
182
+ return useEditDocument(docHandle)
183
+ } catch (e) {
184
+ return e
185
+ }
186
+ })
222
187
 
223
188
  // When the document is not ready, the hook throws the promise from resolveDocument.
224
189
  expect(result.current).toBe(resolveDocPromise)
@@ -10,7 +10,7 @@ import {type SanityDocument} from 'groq'
10
10
  import {useCallback} from 'react'
11
11
 
12
12
  import {useSanityInstance} from '../context/useSanityInstance'
13
- import {useNormalizedSourceOptions} from '../helpers/useNormalizedSourceOptions'
13
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
14
14
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
15
15
  import {useApplyDocumentActions} from './useApplyDocumentActions'
16
16
 
@@ -286,9 +286,9 @@ export function useEditDocument({
286
286
  path,
287
287
  ...doc
288
288
  }: DocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
289
- const instance = useSanityInstance(doc)
289
+ const instance = useSanityInstance()
290
290
  trackHookUsage(instance, 'useEditDocument')
291
- const normalizedDoc = useNormalizedSourceOptions(doc)
291
+ const normalizedDoc = useNormalizedResourceOptions(doc)
292
292
 
293
293
  const apply = useApplyDocumentActions()
294
294
  const isDocumentReady = useCallback(