@sanity/sdk-react 0.0.0-alpha.8 → 0.0.0-chore-react-18-compat.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 (100) hide show
  1. package/README.md +33 -126
  2. package/dist/index.d.ts +4811 -2
  3. package/dist/index.js +1069 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +23 -45
  6. package/src/_exports/index.ts +66 -10
  7. package/src/components/Login/LoginLinks.test.tsx +90 -0
  8. package/src/components/Login/LoginLinks.tsx +58 -0
  9. package/src/components/SDKProvider.test.tsx +79 -0
  10. package/src/components/SDKProvider.tsx +42 -0
  11. package/src/components/SanityApp.test.tsx +104 -2
  12. package/src/components/SanityApp.tsx +54 -17
  13. package/src/components/auth/AuthBoundary.test.tsx +4 -4
  14. package/src/components/auth/AuthBoundary.tsx +13 -3
  15. package/src/components/auth/Login.test.tsx +1 -1
  16. package/src/components/auth/Login.tsx +11 -26
  17. package/src/components/auth/LoginCallback.test.tsx +3 -3
  18. package/src/components/auth/LoginCallback.tsx +8 -11
  19. package/src/components/auth/LoginError.tsx +12 -8
  20. package/src/components/auth/LoginFooter.tsx +13 -20
  21. package/src/components/auth/LoginLayout.tsx +8 -9
  22. package/src/components/auth/authTestHelpers.tsx +1 -8
  23. package/src/components/utils.ts +22 -0
  24. package/src/context/SanityInstanceContext.ts +4 -0
  25. package/src/context/SanityProvider.test.tsx +1 -1
  26. package/src/context/SanityProvider.tsx +10 -8
  27. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  28. package/src/hooks/auth/useAuthState.tsx +0 -2
  29. package/src/hooks/auth/useCurrentUser.tsx +27 -20
  30. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  31. package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
  32. package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
  33. package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +6 -6
  34. package/src/hooks/auth/useLogOut.test.tsx +2 -2
  35. package/src/hooks/client/useClient.ts +9 -30
  36. package/src/hooks/comlink/useFrameConnection.test.tsx +55 -10
  37. package/src/hooks/comlink/useFrameConnection.ts +39 -43
  38. package/src/hooks/comlink/useManageFavorite.test.ts +111 -0
  39. package/src/hooks/comlink/useManageFavorite.ts +130 -0
  40. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +81 -0
  41. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +106 -0
  42. package/src/hooks/comlink/useWindowConnection.test.ts +53 -12
  43. package/src/hooks/comlink/useWindowConnection.ts +69 -29
  44. package/src/hooks/context/useSanityInstance.test.tsx +1 -1
  45. package/src/hooks/context/useSanityInstance.ts +21 -5
  46. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +178 -0
  47. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +123 -0
  48. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +278 -0
  49. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +92 -0
  50. package/src/hooks/datasets/useDatasets.ts +40 -0
  51. package/src/hooks/document/useApplyDocumentActions.test.ts +25 -0
  52. package/src/hooks/document/useApplyDocumentActions.ts +75 -0
  53. package/src/hooks/document/useDocument.test.ts +81 -0
  54. package/src/hooks/document/useDocument.ts +107 -0
  55. package/src/hooks/document/useDocumentEvent.test.ts +63 -0
  56. package/src/hooks/document/useDocumentEvent.ts +54 -0
  57. package/src/hooks/document/useDocumentPermissions.ts +84 -0
  58. package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
  59. package/src/hooks/document/useDocumentSyncStatus.ts +33 -0
  60. package/src/hooks/document/useEditDocument.test.ts +179 -0
  61. package/src/hooks/document/useEditDocument.ts +195 -0
  62. package/src/hooks/documents/useDocuments.test.tsx +152 -0
  63. package/src/hooks/documents/useDocuments.ts +174 -0
  64. package/src/hooks/helpers/createCallbackHook.tsx +3 -2
  65. package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
  66. package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
  67. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +259 -0
  68. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +290 -0
  69. package/src/hooks/preview/usePreview.test.tsx +6 -6
  70. package/src/hooks/preview/usePreview.tsx +12 -9
  71. package/src/hooks/projection/useProjection.test.tsx +218 -0
  72. package/src/hooks/projection/useProjection.ts +147 -0
  73. package/src/hooks/projects/useProject.ts +48 -0
  74. package/src/hooks/projects/useProjects.ts +45 -0
  75. package/src/hooks/query/useQuery.test.tsx +188 -0
  76. package/src/hooks/query/useQuery.ts +103 -0
  77. package/src/hooks/users/useUsers.test.ts +163 -0
  78. package/src/hooks/users/useUsers.ts +107 -0
  79. package/src/utils/getEnv.ts +21 -0
  80. package/src/version.ts +8 -0
  81. package/dist/_chunks-es/context.js +0 -8
  82. package/dist/_chunks-es/context.js.map +0 -1
  83. package/dist/_chunks-es/useLogOut.js +0 -44
  84. package/dist/_chunks-es/useLogOut.js.map +0 -1
  85. package/dist/components.d.ts +0 -111
  86. package/dist/components.js +0 -153
  87. package/dist/components.js.map +0 -1
  88. package/dist/context.d.ts +0 -45
  89. package/dist/context.js +0 -5
  90. package/dist/context.js.map +0 -1
  91. package/dist/hooks.d.ts +0 -3485
  92. package/dist/hooks.js +0 -167
  93. package/dist/hooks.js.map +0 -1
  94. package/src/_exports/components.ts +0 -2
  95. package/src/_exports/context.ts +0 -2
  96. package/src/_exports/hooks.ts +0 -27
  97. package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
  98. package/src/hooks/client/useClient.test.tsx +0 -130
  99. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  100. package/src/hooks/documentCollection/useDocuments.ts +0 -135
@@ -0,0 +1,278 @@
1
+ import {type Message, type Status} from '@sanity/comlink'
2
+ import {renderHook, waitFor} from '@testing-library/react'
3
+ import {describe, expect, it, vi} from 'vitest'
4
+
5
+ import {useWindowConnection, type WindowConnection} from '../comlink/useWindowConnection'
6
+ import {useStudioWorkspacesByResourceId} from './useStudioWorkspacesByResourceId'
7
+
8
+ vi.mock('../comlink/useWindowConnection', () => ({
9
+ useWindowConnection: vi.fn(),
10
+ }))
11
+
12
+ const mockWorkspaceData = {
13
+ context: {
14
+ availableResources: [
15
+ {
16
+ projectId: 'project1',
17
+ workspaces: [
18
+ {
19
+ name: 'workspace1',
20
+ title: 'Workspace 1',
21
+ basePath: '/workspace1',
22
+ dataset: 'dataset1',
23
+ userApplicationId: 'user1',
24
+ url: 'https://test.sanity.studio',
25
+ _ref: 'user1-workspace1',
26
+ },
27
+ {
28
+ name: 'workspace2',
29
+ title: 'Workspace 2',
30
+ basePath: '/workspace2',
31
+ dataset: 'dataset1',
32
+ userApplicationId: 'user1',
33
+ url: 'https://test.sanity.studio',
34
+ _ref: 'user1-workspace2',
35
+ },
36
+ ],
37
+ },
38
+ {
39
+ projectId: 'project2',
40
+ workspaces: [
41
+ {
42
+ name: 'workspace3',
43
+ title: 'Workspace 3',
44
+ basePath: '/workspace3',
45
+ dataset: 'dataset2',
46
+ userApplicationId: 'user2',
47
+ url: 'https://test.sanity.studio',
48
+ _ref: 'user2-workspace3',
49
+ },
50
+ ],
51
+ },
52
+ {
53
+ // Project without workspaces
54
+ projectId: 'project3',
55
+ workspaces: [],
56
+ },
57
+ ],
58
+ },
59
+ }
60
+
61
+ describe('useStudioWorkspacesByResourceId', () => {
62
+ it('should return empty workspaces and connected=false when not connected', async () => {
63
+ // Create a mock that captures the onStatus callback
64
+ let capturedOnStatus: ((status: Status) => void) | undefined
65
+
66
+ vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
67
+ capturedOnStatus = onStatus
68
+
69
+ return {
70
+ fetch: undefined,
71
+ sendMessage: vi.fn(),
72
+ } as unknown as WindowConnection<Message>
73
+ })
74
+
75
+ const {result} = renderHook(() => useStudioWorkspacesByResourceId())
76
+
77
+ // Call onStatus with 'idle' to simulate not connected
78
+ if (capturedOnStatus) capturedOnStatus('idle')
79
+
80
+ expect(result.current).toEqual({
81
+ workspacesByResourceId: {},
82
+ error: null,
83
+ isConnected: false,
84
+ })
85
+ })
86
+
87
+ it('should process workspaces into lookup by projectId:dataset', async () => {
88
+ const mockFetch = vi.fn().mockResolvedValue(mockWorkspaceData)
89
+ let capturedOnStatus: ((status: Status) => void) | undefined
90
+
91
+ vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
92
+ capturedOnStatus = onStatus
93
+
94
+ return {
95
+ fetch: mockFetch,
96
+ sendMessage: vi.fn(),
97
+ } as unknown as WindowConnection<Message>
98
+ })
99
+
100
+ const {result} = renderHook(() => useStudioWorkspacesByResourceId())
101
+
102
+ // Call onStatus with 'connected' to simulate connected state
103
+ if (capturedOnStatus) capturedOnStatus('connected')
104
+
105
+ await waitFor(() => {
106
+ expect(result.current.workspacesByResourceId).toEqual({
107
+ 'project1:dataset1': [
108
+ {
109
+ name: 'workspace1',
110
+ title: 'Workspace 1',
111
+ basePath: '/workspace1',
112
+ dataset: 'dataset1',
113
+ userApplicationId: 'user1',
114
+ url: 'https://test.sanity.studio',
115
+ _ref: 'user1-workspace1',
116
+ },
117
+ {
118
+ name: 'workspace2',
119
+ title: 'Workspace 2',
120
+ basePath: '/workspace2',
121
+ dataset: 'dataset1',
122
+ userApplicationId: 'user1',
123
+ url: 'https://test.sanity.studio',
124
+ _ref: 'user1-workspace2',
125
+ },
126
+ ],
127
+ 'project2:dataset2': [
128
+ {
129
+ name: 'workspace3',
130
+ title: 'Workspace 3',
131
+ basePath: '/workspace3',
132
+ dataset: 'dataset2',
133
+ userApplicationId: 'user2',
134
+ url: 'https://test.sanity.studio',
135
+ _ref: 'user2-workspace3',
136
+ },
137
+ ],
138
+ })
139
+ expect(result.current.error).toBeNull()
140
+ expect(result.current.isConnected).toBe(true)
141
+ })
142
+
143
+ expect(mockFetch).toHaveBeenCalledWith(
144
+ 'dashboard/v1/bridge/context',
145
+ undefined,
146
+ expect.any(Object),
147
+ )
148
+ })
149
+
150
+ it('should handle fetch errors', async () => {
151
+ const mockFetch = vi.fn().mockRejectedValue(new Error('Failed to fetch'))
152
+ let capturedOnStatus: ((status: Status) => void) | undefined
153
+
154
+ vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
155
+ capturedOnStatus = onStatus
156
+
157
+ return {
158
+ fetch: mockFetch,
159
+ sendMessage: vi.fn(),
160
+ } as unknown as WindowConnection<Message>
161
+ })
162
+
163
+ const {result} = renderHook(() => useStudioWorkspacesByResourceId())
164
+
165
+ // Call onStatus with 'connected' to simulate connected state
166
+ if (capturedOnStatus) capturedOnStatus('connected')
167
+
168
+ await waitFor(() => {
169
+ expect(result.current.workspacesByResourceId).toEqual({})
170
+ expect(result.current.error).toBe('Failed to fetch workspaces')
171
+ expect(result.current.isConnected).toBe(true)
172
+ })
173
+ })
174
+
175
+ it('should handle AbortError silently', async () => {
176
+ const abortError = new Error('Aborted')
177
+ abortError.name = 'AbortError'
178
+ const mockFetch = vi.fn().mockRejectedValue(abortError)
179
+ let capturedOnStatus: ((status: Status) => void) | undefined
180
+
181
+ vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
182
+ capturedOnStatus = onStatus
183
+
184
+ return {
185
+ fetch: mockFetch,
186
+ sendMessage: vi.fn(),
187
+ } as unknown as WindowConnection<Message>
188
+ })
189
+
190
+ const {result} = renderHook(() => useStudioWorkspacesByResourceId())
191
+
192
+ // Call onStatus with 'connected' to simulate connected state
193
+ if (capturedOnStatus) capturedOnStatus('connected')
194
+
195
+ await waitFor(() => {
196
+ expect(result.current.workspacesByResourceId).toEqual({})
197
+ expect(result.current.error).toBeNull()
198
+ expect(result.current.isConnected).toBe(true)
199
+ })
200
+ })
201
+
202
+ it('should handle projects without workspaces', async () => {
203
+ const mockFetch = vi.fn().mockResolvedValue({
204
+ context: {
205
+ availableResources: [
206
+ {
207
+ projectId: 'project1',
208
+ workspaces: [],
209
+ },
210
+ ],
211
+ },
212
+ })
213
+ let capturedOnStatus: ((status: Status) => void) | undefined
214
+
215
+ vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
216
+ capturedOnStatus = onStatus
217
+
218
+ return {
219
+ fetch: mockFetch,
220
+ sendMessage: vi.fn(),
221
+ } as unknown as WindowConnection<Message>
222
+ })
223
+
224
+ const {result} = renderHook(() => useStudioWorkspacesByResourceId())
225
+
226
+ // Call onStatus with 'connected' to simulate connected state
227
+ if (capturedOnStatus) capturedOnStatus('connected')
228
+
229
+ await waitFor(() => {
230
+ expect(result.current.workspacesByResourceId).toEqual({})
231
+ expect(result.current.error).toBeNull()
232
+ expect(result.current.isConnected).toBe(true)
233
+ })
234
+ })
235
+
236
+ it('should handle projects without projectId', async () => {
237
+ const mockFetch = vi.fn().mockResolvedValue({
238
+ context: {
239
+ availableResources: [
240
+ {
241
+ workspaces: [
242
+ {
243
+ name: 'workspace1',
244
+ title: 'Workspace 1',
245
+ basePath: '/workspace1',
246
+ dataset: 'dataset1',
247
+ userApplicationId: 'user1',
248
+ url: 'https://test.sanity.studio',
249
+ _ref: 'user1-workspace1',
250
+ },
251
+ ],
252
+ },
253
+ ],
254
+ },
255
+ })
256
+ let capturedOnStatus: ((status: Status) => void) | undefined
257
+
258
+ vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
259
+ capturedOnStatus = onStatus
260
+
261
+ return {
262
+ fetch: mockFetch,
263
+ sendMessage: vi.fn(),
264
+ } as unknown as WindowConnection<Message>
265
+ })
266
+
267
+ const {result} = renderHook(() => useStudioWorkspacesByResourceId())
268
+
269
+ // Call onStatus with 'connected' to simulate connected state
270
+ if (capturedOnStatus) capturedOnStatus('connected')
271
+
272
+ await waitFor(() => {
273
+ expect(result.current.workspacesByResourceId).toEqual({})
274
+ expect(result.current.error).toBeNull()
275
+ expect(result.current.isConnected).toBe(true)
276
+ })
277
+ })
278
+ })
@@ -0,0 +1,92 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
3
+ import {useEffect, useState} from 'react'
4
+
5
+ import {useWindowConnection} from '../comlink/useWindowConnection'
6
+
7
+ interface Workspace {
8
+ name: string
9
+ title: string
10
+ basePath: string
11
+ dataset: string
12
+ userApplicationId: string
13
+ url: string
14
+ _ref: string
15
+ }
16
+
17
+ interface WorkspacesByResourceId {
18
+ [key: string]: Workspace[] // key format: `${projectId}:${dataset}`
19
+ }
20
+
21
+ interface StudioWorkspacesResult {
22
+ workspacesByResourceId: WorkspacesByResourceId
23
+ error: string | null
24
+ isConnected: boolean
25
+ }
26
+
27
+ /**
28
+ * Hook that fetches studio workspaces and organizes them by projectId:dataset
29
+ * @internal
30
+ */
31
+ export function useStudioWorkspacesByResourceId(): StudioWorkspacesResult {
32
+ const [workspacesByResourceId, setWorkspacesByResourceId] = useState<WorkspacesByResourceId>({})
33
+ const [status, setStatus] = useState<Status>('idle')
34
+ const [error, setError] = useState<string | null>(null)
35
+
36
+ const {fetch} = useWindowConnection({
37
+ name: SDK_NODE_NAME,
38
+ connectTo: SDK_CHANNEL_NAME,
39
+ onStatus: setStatus,
40
+ })
41
+
42
+ // Once computed, this should probably be in a store and poll for changes
43
+ // However, our stores are currently being refactored
44
+ useEffect(() => {
45
+ if (!fetch || status !== 'connected') return
46
+
47
+ async function fetchWorkspaces(signal: AbortSignal) {
48
+ try {
49
+ const data = await fetch<{
50
+ context: {availableResources: Array<{projectId: string; workspaces: Workspace[]}>}
51
+ }>('dashboard/v1/bridge/context', undefined, {signal})
52
+
53
+ const workspaceMap: WorkspacesByResourceId = {}
54
+
55
+ data.context.availableResources.forEach((resource) => {
56
+ if (!resource.projectId || !resource.workspaces?.length) return
57
+
58
+ resource.workspaces.forEach((workspace) => {
59
+ const key = `${resource.projectId}:${workspace.dataset}`
60
+ if (!workspaceMap[key]) {
61
+ workspaceMap[key] = []
62
+ }
63
+ workspaceMap[key].push(workspace)
64
+ })
65
+ })
66
+
67
+ setWorkspacesByResourceId(workspaceMap)
68
+ setError(null)
69
+ } catch (err: unknown) {
70
+ if (err instanceof Error) {
71
+ if (err.name === 'AbortError') {
72
+ return
73
+ }
74
+ setError('Failed to fetch workspaces')
75
+ }
76
+ }
77
+ }
78
+
79
+ const controller = new AbortController()
80
+ fetchWorkspaces(controller.signal)
81
+
82
+ return () => {
83
+ controller.abort()
84
+ }
85
+ }, [fetch, status])
86
+
87
+ return {
88
+ workspacesByResourceId,
89
+ error,
90
+ isConnected: status === 'connected',
91
+ }
92
+ }
@@ -0,0 +1,40 @@
1
+ import {type DatasetsResponse} from '@sanity/client'
2
+ import {getDatasetsState, resolveDatasets, type SanityInstance, type StateSource} from '@sanity/sdk'
3
+
4
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
+
6
+ type UseDatasets = {
7
+ /**
8
+ *
9
+ * Returns metadata for each dataset the current user has access to.
10
+ *
11
+ * @category Datasets
12
+ * @returns The metadata for your the datasets
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const datasets = useDatasets()
17
+ *
18
+ * return (
19
+ * <select>
20
+ * {datasets.map((dataset) => (
21
+ * <option key={dataset.name}>{dataset.name}</option>
22
+ * ))}
23
+ * </select>
24
+ * )
25
+ * ```
26
+ *
27
+ */
28
+ (): DatasetsResponse
29
+ }
30
+
31
+ /**
32
+ * @public
33
+ * @function
34
+ */
35
+ export const useDatasets: UseDatasets = createStateSourceHook({
36
+ // remove `undefined` since we're suspending when that is the case
37
+ getState: getDatasetsState as (instance: SanityInstance) => StateSource<DatasetsResponse>,
38
+ shouldSuspend: (instance) => getDatasetsState(instance).getCurrent() === undefined,
39
+ suspender: resolveDatasets,
40
+ })
@@ -0,0 +1,25 @@
1
+ import {applyDocumentActions, createDocument, type ResourceId} from '@sanity/sdk'
2
+ import {describe, it} from 'vitest'
3
+
4
+ import {createCallbackHook} from '../helpers/createCallbackHook'
5
+
6
+ vi.mock('../helpers/createCallbackHook', () => ({
7
+ createCallbackHook: vi.fn((cb) => () => cb),
8
+ }))
9
+ vi.mock('@sanity/sdk', async (importOriginal) => {
10
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
11
+ return {...original, applyDocumentActions: vi.fn()}
12
+ })
13
+
14
+ describe('useApplyDocumentActions', () => {
15
+ it('calls `createCallbackHook` with `applyDocumentActions`', async () => {
16
+ const {useApplyDocumentActions} = await import('./useApplyDocumentActions')
17
+ const resourceId: ResourceId = 'project1.dataset1'
18
+ expect(createCallbackHook).not.toHaveBeenCalled()
19
+
20
+ expect(applyDocumentActions).not.toHaveBeenCalled()
21
+ const apply = useApplyDocumentActions(resourceId)
22
+ apply(createDocument({_type: 'author'}))
23
+ expect(applyDocumentActions).toHaveBeenCalledWith(createDocument({_type: 'author'}))
24
+ })
25
+ })
@@ -0,0 +1,75 @@
1
+ import {
2
+ type ActionsResult,
3
+ applyDocumentActions,
4
+ type ApplyDocumentActionsOptions,
5
+ type DocumentAction,
6
+ type ResourceId,
7
+ } from '@sanity/sdk'
8
+ import {type SanityDocument} from '@sanity/types'
9
+
10
+ import {createCallbackHook} from '../helpers/createCallbackHook'
11
+
12
+ /**
13
+ *
14
+ * @beta
15
+ *
16
+ * Provides a callback for applying one or more actions to a document.
17
+ *
18
+ * @category Documents
19
+ * @param resourceId - The resource ID of the document to apply actions to. If not provided, the document will use the default resource.
20
+ * @returns A function that takes one more more {@link DocumentAction}s and returns a promise that resolves to an {@link ActionsResult}.
21
+ * @example Publish or unpublish a document
22
+ * ```
23
+ * import { publishDocument, unpublishDocument } from '@sanity/sdk'
24
+ * import { useApplyDocumentActions } from '@sanity/sdk-react'
25
+ *
26
+ * const apply = useApplyDocumentActions()
27
+ * const myDocument = { _id: 'my-document-id', _type: 'my-document-type' }
28
+ *
29
+ * return (
30
+ * <button onClick={() => apply(publishDocument(myDocument))}>Publish</button>
31
+ * <button onClick={() => apply(unpublishDocument(myDocument))}>Unpublish</button>
32
+ * )
33
+ * ```
34
+ *
35
+ * @example Create and publish a new document
36
+ * ```
37
+ * import { createDocument, publishDocument } from '@sanity/sdk'
38
+ * import { useApplyDocumentActions } from '@sanity/sdk-react'
39
+ *
40
+ * const apply = useApplyDocumentActions()
41
+ *
42
+ * const handleCreateAndPublish = () => {
43
+ * const handle = { _id: window.crypto.randomUUID(), _type: 'my-document-type' }
44
+ * apply([
45
+ * createDocument(handle),
46
+ * publishDocument(handle),
47
+ * ])
48
+ * }
49
+ *
50
+ * return (
51
+ * <button onClick={handleCreateAndPublish}>
52
+ * I’m feeling lucky
53
+ * </button>
54
+ * )
55
+ * ```
56
+ */
57
+ export function useApplyDocumentActions(
58
+ resourceId?: ResourceId,
59
+ ): <TDocument extends SanityDocument>(
60
+ action: DocumentAction<TDocument> | DocumentAction<TDocument>[],
61
+ options?: ApplyDocumentActionsOptions,
62
+ ) => Promise<ActionsResult<TDocument>>
63
+
64
+ /** @beta */
65
+ export function useApplyDocumentActions(
66
+ resourceId?: ResourceId,
67
+ ): (
68
+ action: DocumentAction | DocumentAction[],
69
+ options?: ApplyDocumentActionsOptions,
70
+ ) => Promise<ActionsResult> {
71
+ return _useApplyDocumentActions(resourceId)()
72
+ }
73
+
74
+ const _useApplyDocumentActions = (resourceId?: ResourceId) =>
75
+ createCallbackHook(applyDocumentActions, resourceId)
@@ -0,0 +1,81 @@
1
+ // tests/useDocument.test.ts
2
+ import {
3
+ createSanityInstance,
4
+ getDocumentState,
5
+ resolveDocument,
6
+ type StateSource,
7
+ } from '@sanity/sdk'
8
+ import {type SanityDocument} from '@sanity/types'
9
+ import {renderHook} from '@testing-library/react'
10
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
11
+
12
+ import {useSanityInstance} from '../context/useSanityInstance'
13
+ import {useDocument} from './useDocument'
14
+
15
+ vi.mock('@sanity/sdk', async (importOriginal) => {
16
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
17
+ return {...original, getDocumentState: vi.fn(), resolveDocument: vi.fn()}
18
+ })
19
+
20
+ vi.mock('../context/useSanityInstance', () => ({
21
+ useSanityInstance: vi.fn(),
22
+ }))
23
+
24
+ // Create a fake instance to be returned by useSanityInstance.
25
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
26
+ const doc: SanityDocument = {
27
+ _id: 'doc1',
28
+ foo: 'bar',
29
+ _type: 'book',
30
+ _rev: 'tx0',
31
+ _createdAt: '2025-02-06T00:11:00.000Z',
32
+ _updatedAt: '2025-02-06T00:11:00.000Z',
33
+ }
34
+
35
+ describe('useDocument hook', () => {
36
+ beforeEach(() => {
37
+ vi.resetAllMocks()
38
+ vi.mocked(useSanityInstance).mockReturnValue(instance)
39
+ })
40
+
41
+ it('returns the current document when ready (without a path)', () => {
42
+ const getCurrent = vi.fn().mockReturnValue(doc)
43
+ const subscribe = vi.fn().mockReturnValue(vi.fn())
44
+ vi.mocked(getDocumentState).mockReturnValue({
45
+ getCurrent,
46
+ subscribe,
47
+ } as unknown as StateSource<unknown>)
48
+
49
+ const {result} = renderHook(() => useDocument({_id: 'doc1', _type: 'book'}))
50
+
51
+ expect(result.current).toEqual(doc)
52
+ expect(getCurrent).toHaveBeenCalled()
53
+ expect(subscribe).toHaveBeenCalled()
54
+ })
55
+
56
+ it('throws a promise (suspends) when the document is not ready', () => {
57
+ const getCurrent = vi.fn().mockReturnValue(undefined)
58
+ const subscribe = vi.fn().mockReturnValue(vi.fn())
59
+ vi.mocked(getDocumentState).mockReturnValue({
60
+ getCurrent,
61
+ subscribe,
62
+ } as unknown as StateSource<unknown>)
63
+
64
+ const resolveDocPromise = Promise.resolve(doc)
65
+
66
+ // Also, simulate resolveDocument to return a known promise.
67
+ vi.mocked(resolveDocument).mockReturnValue(resolveDocPromise)
68
+
69
+ // Render the hook and capture the thrown promise.
70
+ const {result} = renderHook(() => {
71
+ try {
72
+ return useDocument({_id: 'doc1', _type: 'book'})
73
+ } catch (e) {
74
+ return e
75
+ }
76
+ })
77
+
78
+ // When the document is not ready, the hook throws the promise from resolveDocument.
79
+ expect(result.current).toBe(resolveDocPromise)
80
+ })
81
+ })
@@ -0,0 +1,107 @@
1
+ import {
2
+ type DocumentHandle,
3
+ getDocumentState,
4
+ getResourceId,
5
+ type JsonMatch,
6
+ type JsonMatchPath,
7
+ resolveDocument,
8
+ } from '@sanity/sdk'
9
+ import {type SanityDocument} from '@sanity/types'
10
+
11
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
+
13
+ /**
14
+ * @beta
15
+ *
16
+ * ## useDocument(doc, path)
17
+ * Read and subscribe to nested values in a document
18
+ * @category Documents
19
+ * @param doc - The document to read state from. If you pass a `DocumentHandle` with a `resourceId` in the DocumentResourceId format (`document:projectId.dataset:documentId`)
20
+ * the document will be read from the specified Sanity project and dataset that is included in the handle. If no `resourceId` is provided, the default project and dataset from your `SanityApp` configuration will be used.
21
+ * @param path - The path to the nested value to read from
22
+ * @returns The value at the specified path
23
+ * @example
24
+ * ```tsx
25
+ * import {type DocumentHandle, useDocument} from '@sanity/sdk-react'
26
+ *
27
+ * function OrderLink({documentHandle}: {documentHandle: DocumentHandle}) {
28
+ * const title = useDocument(documentHandle, 'title')
29
+ * const id = useDocument(documentHandle, '_id')
30
+ *
31
+ * return (
32
+ * <a href=`/order/${id}`>Order {title} today!</a>
33
+ * )
34
+ * }
35
+ * ```
36
+ *
37
+ */
38
+ export function useDocument<
39
+ TDocument extends SanityDocument,
40
+ TPath extends JsonMatchPath<TDocument>,
41
+ >(doc: DocumentHandle<TDocument>, path: TPath): JsonMatch<TDocument, TPath> | undefined
42
+
43
+ /**
44
+ * @beta
45
+ * ## useDocument(doc)
46
+ * Read and subscribe to an entire document
47
+ * @param doc - The document to read state from
48
+ * @returns The document state as an object
49
+ * @example
50
+ * ```tsx
51
+ * import {type SanityDocument, type DocumentHandle, useDocument} from '@sanity/sdk-react'
52
+ *
53
+ * interface Book extends SanityDocument {
54
+ * title: string
55
+ * author: string
56
+ * summary: string
57
+ * }
58
+ *
59
+ * function DocumentView({documentHandle}: {documentHandle: DocumentHandle}) {
60
+ * const book = useDocument<Book>(documentHandle)
61
+ *
62
+ * return (
63
+ * <article>
64
+ * <h1>{book?.title}</h1>
65
+ * <address>By {book?.author}</address>
66
+ *
67
+ * <h2>Summary</h2>
68
+ * {book?.summary}
69
+ *
70
+ * <h2>Order</h2>
71
+ * <a href=`/order/${book._id}`>Order {book?.title} today!</a>
72
+ * </article>
73
+ * )
74
+ * }
75
+ * ```
76
+ *
77
+ */
78
+ export function useDocument<TDocument extends SanityDocument>(
79
+ doc: DocumentHandle<TDocument>,
80
+ ): TDocument | null
81
+
82
+ /**
83
+ * @beta
84
+ * Reads and subscribes to a document’s realtime state, incorporating both local and remote changes.
85
+ * When called with a `path` argument, the hook will return the nested value’s state.
86
+ * When called without a `path` argument, the entire document’s state will be returned.
87
+ *
88
+ * @remarks
89
+ * `useDocument` is designed to be used within a realtime context in which local updates to documents
90
+ * need to be displayed before they are persisted to the remote copy. This can be useful within a collaborative
91
+ * or realtime editing interface where local changes need to be reflected immediately.
92
+ *
93
+ * However, this hook can be too resource intensive for applications where static document values simply
94
+ * need to be displayed (or when changes to documents don’t need to be reflected immediately);
95
+ * consider using `usePreview` or `useQuery` for these use cases instead. These hooks leverage the Sanity
96
+ * Live Content API to provide a more efficient way to read and subscribe to document state.
97
+ */
98
+ export function useDocument(doc: DocumentHandle, path?: string): unknown {
99
+ return _useDocument(doc, path)
100
+ }
101
+
102
+ const _useDocument = createStateSourceHook<[doc: DocumentHandle, path?: string], unknown>({
103
+ getState: getDocumentState,
104
+ shouldSuspend: (instance, doc) => getDocumentState(instance, doc._id).getCurrent() === undefined,
105
+ suspender: resolveDocument,
106
+ getResourceId: (doc) => getResourceId(doc.resourceId),
107
+ })