@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.
- package/dist/index.d.ts +338 -215
- package/dist/index.js +564 -342
- package/dist/index.js.map +1 -1
- package/package.json +9 -14
- package/src/_exports/index.ts +2 -0
- package/src/_exports/sdk-react.ts +8 -0
- package/src/components/SDKProvider.test.tsx +5 -12
- package/src/components/SDKProvider.tsx +58 -28
- package/src/components/SanityApp.tsx +2 -2
- package/src/components/auth/AuthBoundary.tsx +8 -1
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +100 -56
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +55 -0
- package/src/constants.ts +5 -0
- package/src/context/DefaultResourceContext.ts +10 -0
- package/src/context/PerspectiveContext.ts +12 -0
- package/src/context/ResourceProvider.test.tsx +2 -2
- package/src/context/ResourceProvider.tsx +56 -51
- package/src/context/ResourcesContext.tsx +7 -0
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/hooks/agent/agentActions.ts +55 -38
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +42 -111
- package/src/hooks/context/useSanityInstance.ts +28 -50
- package/src/hooks/dashboard/useDispatchIntent.test.ts +11 -7
- package/src/hooks/dashboard/useDispatchIntent.ts +7 -7
- package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -17
- package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +46 -81
- package/src/hooks/document/useApplyDocumentActions.ts +33 -67
- package/src/hooks/document/useDocument.ts +4 -6
- package/src/hooks/document/useDocumentEvent.ts +8 -7
- package/src/hooks/document/useDocumentPermissions.test.tsx +60 -152
- package/src/hooks/document/useDocumentPermissions.ts +78 -55
- package/src/hooks/document/useDocumentSyncStatus.ts +2 -2
- package/src/hooks/document/useEditDocument.test.tsx +25 -60
- package/src/hooks/document/useEditDocument.ts +3 -3
- package/src/hooks/documents/useDocuments.ts +19 -11
- package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +169 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +2 -2
- package/src/hooks/organizations/useOrganization.test-d.ts +53 -0
- package/src/hooks/organizations/useOrganization.test.ts +65 -0
- package/src/hooks/organizations/useOrganization.ts +40 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +55 -0
- package/src/hooks/organizations/useOrganizations.test.ts +85 -0
- package/src/hooks/organizations/useOrganizations.ts +45 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +29 -14
- package/src/hooks/presence/usePresence.test.tsx +56 -9
- package/src/hooks/presence/usePresence.ts +16 -4
- package/src/hooks/preview/useDocumentPreview.tsx +8 -10
- package/src/hooks/projection/useDocumentProjection.ts +7 -9
- package/src/hooks/projects/useProject.test-d.ts +49 -0
- package/src/hooks/projects/useProject.ts +33 -41
- package/src/hooks/projects/useProjects.test-d.ts +49 -0
- package/src/hooks/projects/useProjects.ts +17 -23
- package/src/hooks/query/useQuery.ts +11 -10
- package/src/hooks/releases/useActiveReleases.ts +14 -14
- package/src/hooks/releases/usePerspective.ts +11 -16
- package/src/hooks/users/useUser.ts +1 -1
- package/src/hooks/users/useUsers.ts +1 -1
- package/src/context/SourcesContext.tsx +0 -7
- 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 {
|
|
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
|
-
|
|
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:
|
|
50
|
-
let mockGetCurrent:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
139
|
+
it('should throw an error if actions have mismatched datasets', () => {
|
|
218
140
|
const actions = [
|
|
219
141
|
mockAction,
|
|
220
142
|
{
|
|
221
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
159
|
+
documentId: 'doc2',
|
|
248
160
|
documentType: 'article',
|
|
249
|
-
|
|
161
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
250
162
|
},
|
|
251
|
-
mockAction,
|
|
252
163
|
]
|
|
253
164
|
|
|
254
165
|
expect(() => {
|
|
255
|
-
renderHook(() => useDocumentPermissions(actions)
|
|
256
|
-
|
|
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} =
|
|
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(
|
|
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 {
|
|
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:
|
|
96
|
+
actionOrActions:
|
|
97
|
+
| WithResourceNameSupport<DocumentAction>
|
|
98
|
+
| WithResourceNameSupport<DocumentAction>[],
|
|
87
99
|
): DocumentPermissionsResult {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
() =>
|
|
140
|
-
[
|
|
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
|
-
|
|
145
|
-
filter((result) => result !== undefined),
|
|
146
|
-
),
|
|
174
|
+
stateSource!.observable.pipe(filter((permissions) => permissions !== undefined)),
|
|
147
175
|
)
|
|
148
176
|
}
|
|
149
177
|
|
|
150
|
-
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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(
|
|
88
|
-
expect(apply).toHaveBeenCalledWith(editDocument(
|
|
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(
|
|
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(
|
|
139
|
-
expect(apply).toHaveBeenCalledWith(editDocument(
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 {
|
|
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(
|
|
289
|
+
const instance = useSanityInstance()
|
|
290
290
|
trackHookUsage(instance, 'useEditDocument')
|
|
291
|
-
const normalizedDoc =
|
|
291
|
+
const normalizedDoc = useNormalizedResourceOptions(doc)
|
|
292
292
|
|
|
293
293
|
const apply = useApplyDocumentActions()
|
|
294
294
|
const isDocumentReady = useCallback(
|