@sanity/sdk-react 2.8.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.
- package/dist/index.d.ts +144 -25
- package/dist/index.js +216 -122
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/_exports/sdk-react.ts +1 -0
- package/src/components/SanityApp.tsx +1 -0
- package/src/context/ResourceProvider.test.tsx +7 -1
- package/src/context/ResourceProvider.tsx +6 -0
- package/src/context/SDKStudioContext.ts +6 -0
- package/src/hooks/dashboard/useDispatchIntent.test.ts +2 -0
- package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
- package/src/hooks/dashboard/useWindowTitle.ts +112 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
- package/src/hooks/document/useApplyDocumentActions.ts +99 -3
- package/src/hooks/document/useDocument.ts +22 -6
- package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
- package/src/hooks/document/useDocumentEvent.ts +10 -3
- package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
- package/src/hooks/document/useDocumentPermissions.ts +22 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
- package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
- package/src/hooks/document/useEditDocument.ts +34 -8
- package/src/hooks/documents/useDocuments.ts +2 -0
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +50 -28
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +2 -0
- package/src/hooks/presence/usePresence.ts +2 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
- package/src/hooks/preview/useDocumentPreview.tsx +39 -55
- package/src/hooks/projection/useDocumentProjection.ts +2 -0
- package/src/hooks/query/useQuery.ts +2 -0
- package/src/hooks/releases/useActiveReleases.ts +32 -13
- package/src/hooks/releases/usePerspective.ts +26 -14
- package/src/hooks/users/useUser.ts +2 -0
- package/src/hooks/users/useUsers.ts +2 -0
|
@@ -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, {
|
|
278
|
+
return applyDocumentActions(actualInstance, {
|
|
279
|
+
actions,
|
|
280
|
+
source,
|
|
281
|
+
...normalizedOptions,
|
|
282
|
+
})
|
|
191
283
|
}
|
|
192
284
|
|
|
193
|
-
return applyDocumentActions(instance, {
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
41
|
-
expect(typeof
|
|
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
|
-
|
|
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
|
|
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,
|
|
93
|
-
|
|
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:
|
|
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', () =>
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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 {useNormalizedSourceOptions} from '../helpers/useNormalizedSourceOptions'
|
|
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 = useNormalizedSourceOptions(doc)
|
|
292
|
+
|
|
267
293
|
const apply = useApplyDocumentActions()
|
|
268
294
|
const isDocumentReady = useCallback(
|
|
269
|
-
() => getDocumentState(instance,
|
|
270
|
-
[instance,
|
|
295
|
+
() => getDocumentState(instance, normalizedDoc).getCurrent() !== undefined,
|
|
296
|
+
[instance, normalizedDoc],
|
|
271
297
|
)
|
|
272
|
-
if (!isDocumentReady()) throw resolveDocument(instance,
|
|
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, {...
|
|
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(
|
|
312
|
+
return apply(editDocument(normalizedDoc, {set: {[currentPath]: nextValue}}))
|
|
287
313
|
}
|
|
288
314
|
|
|
289
|
-
const fullDocState = getDocumentState(instance, {...
|
|
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(
|
|
312
|
-
: editDocument(
|
|
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)
|
|
@@ -9,6 +9,7 @@ import {pick} from 'lodash-es'
|
|
|
9
9
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
10
10
|
|
|
11
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
12
13
|
import {useQuery} from '../query/useQuery'
|
|
13
14
|
|
|
14
15
|
const DEFAULT_BATCH_SIZE = 25
|
|
@@ -207,6 +208,7 @@ export function useDocuments<
|
|
|
207
208
|
TDataset,
|
|
208
209
|
TProjectId
|
|
209
210
|
> {
|
|
211
|
+
useTrackHookUsage('useDocuments')
|
|
210
212
|
const instance = useSanityInstance(options)
|
|
211
213
|
const [limit, setLimit] = useState(batchSize)
|
|
212
214
|
const documentTypes = useMemo(
|