@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.31

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 (131) hide show
  1. package/README.md +6 -100
  2. package/dist/index.d.ts +2390 -2
  3. package/dist/index.js +1119 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +35 -49
  6. package/src/_exports/index.ts +2 -10
  7. package/src/_exports/sdk-react.ts +73 -0
  8. package/src/components/SDKProvider.test.tsx +103 -0
  9. package/src/components/SDKProvider.tsx +52 -0
  10. package/src/components/SanityApp.test.tsx +244 -0
  11. package/src/components/SanityApp.tsx +106 -0
  12. package/src/components/auth/AuthBoundary.test.tsx +204 -29
  13. package/src/components/auth/AuthBoundary.tsx +96 -19
  14. package/src/components/auth/ConfigurationError.ts +22 -0
  15. package/src/components/auth/LoginCallback.test.tsx +22 -24
  16. package/src/components/auth/LoginCallback.tsx +6 -16
  17. package/src/components/auth/LoginError.test.tsx +11 -18
  18. package/src/components/auth/LoginError.tsx +43 -25
  19. package/src/components/utils.ts +22 -0
  20. package/src/context/ResourceProvider.test.tsx +157 -0
  21. package/src/context/ResourceProvider.tsx +111 -0
  22. package/src/context/SanityInstanceContext.ts +4 -0
  23. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  24. package/src/hooks/auth/useAuthState.tsx +4 -5
  25. package/src/hooks/auth/useAuthToken.tsx +1 -1
  26. package/src/hooks/auth/useCurrentUser.tsx +28 -4
  27. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  28. package/src/hooks/auth/useDashboardOrganizationId.tsx +30 -0
  29. package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
  30. package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
  31. package/src/hooks/auth/useLogOut.test.tsx +2 -2
  32. package/src/hooks/auth/useLogOut.tsx +1 -1
  33. package/src/hooks/auth/useLoginUrl.tsx +14 -0
  34. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
  35. package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
  36. package/src/hooks/client/useClient.ts +13 -33
  37. package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
  38. package/src/hooks/comlink/useFrameConnection.ts +107 -0
  39. package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
  40. package/src/hooks/comlink/useManageFavorite.ts +210 -0
  41. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
  42. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
  43. package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
  44. package/src/hooks/comlink/useWindowConnection.ts +123 -0
  45. package/src/hooks/context/useSanityInstance.test.tsx +157 -15
  46. package/src/hooks/context/useSanityInstance.ts +68 -11
  47. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
  48. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
  49. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
  50. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
  51. package/src/hooks/datasets/useDatasets.test.ts +80 -0
  52. package/src/hooks/datasets/useDatasets.ts +52 -0
  53. package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
  54. package/src/hooks/document/useApplyDocumentActions.ts +124 -0
  55. package/src/hooks/document/useDocument.test.ts +118 -0
  56. package/src/hooks/document/useDocument.ts +212 -0
  57. package/src/hooks/document/useDocumentEvent.test.ts +62 -0
  58. package/src/hooks/document/useDocumentEvent.ts +94 -0
  59. package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
  60. package/src/hooks/document/useDocumentPermissions.ts +131 -0
  61. package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
  62. package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
  63. package/src/hooks/document/useEditDocument.test.ts +196 -0
  64. package/src/hooks/document/useEditDocument.ts +314 -0
  65. package/src/hooks/documents/useDocuments.test.tsx +179 -0
  66. package/src/hooks/documents/useDocuments.ts +300 -0
  67. package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
  68. package/src/hooks/helpers/createCallbackHook.tsx +1 -1
  69. package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
  70. package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
  71. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
  72. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
  73. package/src/hooks/preview/usePreview.test.tsx +85 -17
  74. package/src/hooks/preview/usePreview.tsx +81 -22
  75. package/src/hooks/projection/useProjection.test.tsx +283 -0
  76. package/src/hooks/projection/useProjection.ts +232 -0
  77. package/src/hooks/projects/useProject.test.ts +80 -0
  78. package/src/hooks/projects/useProject.ts +51 -0
  79. package/src/hooks/projects/useProjects.test.ts +77 -0
  80. package/src/hooks/projects/useProjects.ts +45 -0
  81. package/src/hooks/query/useQuery.test.tsx +188 -0
  82. package/src/hooks/query/useQuery.ts +193 -0
  83. package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
  84. package/src/hooks/releases/useActiveReleases.ts +39 -0
  85. package/src/hooks/releases/usePerspective.test.tsx +120 -0
  86. package/src/hooks/releases/usePerspective.ts +49 -0
  87. package/src/hooks/users/useUsers.test.tsx +330 -0
  88. package/src/hooks/users/useUsers.ts +120 -0
  89. package/src/utils/getEnv.ts +21 -0
  90. package/src/version.ts +8 -0
  91. package/src/vite-env.d.ts +10 -0
  92. package/dist/_chunks-es/useLogOut.js +0 -44
  93. package/dist/_chunks-es/useLogOut.js.map +0 -1
  94. package/dist/assets/bundle-CcAyERuZ.css +0 -11
  95. package/dist/components.d.ts +0 -259
  96. package/dist/components.js +0 -301
  97. package/dist/components.js.map +0 -1
  98. package/dist/hooks.d.ts +0 -186
  99. package/dist/hooks.js +0 -81
  100. package/dist/hooks.js.map +0 -1
  101. package/src/_exports/components.ts +0 -13
  102. package/src/_exports/hooks.ts +0 -9
  103. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
  104. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
  105. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
  106. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
  107. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
  108. package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
  109. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
  110. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
  111. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
  112. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
  113. package/src/components/Login/LoginLinks.test.tsx +0 -100
  114. package/src/components/Login/LoginLinks.tsx +0 -73
  115. package/src/components/auth/Login.test.tsx +0 -41
  116. package/src/components/auth/Login.tsx +0 -45
  117. package/src/components/auth/LoginFooter.test.tsx +0 -29
  118. package/src/components/auth/LoginFooter.tsx +0 -65
  119. package/src/components/auth/LoginLayout.test.tsx +0 -33
  120. package/src/components/auth/LoginLayout.tsx +0 -81
  121. package/src/components/context/SanityProvider.test.tsx +0 -25
  122. package/src/components/context/SanityProvider.tsx +0 -42
  123. package/src/css/css.config.js +0 -220
  124. package/src/css/paramour.css +0 -2347
  125. package/src/css/styles.css +0 -11
  126. package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
  127. package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
  128. package/src/hooks/auth/useLoginUrls.tsx +0 -51
  129. package/src/hooks/client/useClient.test.tsx +0 -130
  130. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  131. package/src/hooks/documentCollection/useDocuments.ts +0 -87
@@ -1,23 +1,80 @@
1
- import type {SanityInstance} from '@sanity/sdk'
1
+ import {type SanityConfig, type SanityInstance} from '@sanity/sdk'
2
2
  import {useContext} from 'react'
3
3
 
4
- import {SanityInstanceContext} from '../../components/context/SanityProvider'
4
+ import {SanityInstanceContext} from '../../context/SanityInstanceContext'
5
5
 
6
6
  /**
7
- * Hook that provides the current Sanity instance from the context.
8
- * This must be called from within a `SanityProvider` component.
7
+ * Retrieves the current Sanity instance or finds a matching instance from the hierarchy
8
+ *
9
9
  * @public
10
- * @returns the current Sanity instance
11
- * @example
10
+ *
11
+ * @category Platform
12
+ * @param config - Optional configuration to match against when finding an instance
13
+ * @returns The current or matching Sanity instance
14
+ *
15
+ * @remarks
16
+ * This hook accesses the nearest Sanity instance from the React context. When provided with
17
+ * a configuration object, it traverses up the instance hierarchy to find the closest instance
18
+ * that matches the specified configuration using shallow comparison of properties.
19
+ *
20
+ * The hook must be used within a component wrapped by a `ResourceProvider` or `SanityApp`.
21
+ *
22
+ * Use this hook when you need to:
23
+ * - Access the current SanityInstance from context
24
+ * - Find a specific instance with matching project/dataset configuration
25
+ * - Access a parent instance with specific configuration values
26
+ *
27
+ * @example Get the current instance
12
28
  * ```tsx
29
+ * // Get the current instance from context
13
30
  * const instance = useSanityInstance()
31
+ * console.log(instance.config.projectId)
14
32
  * ```
33
+ *
34
+ * @example Find an instance with specific configuration
35
+ * ```tsx
36
+ * // Find an instance matching the given project and dataset
37
+ * const instance = useSanityInstance({
38
+ * projectId: 'abc123',
39
+ * dataset: 'production'
40
+ * })
41
+ *
42
+ * // Use instance for API calls
43
+ * const fetchDocument = (docId) => {
44
+ * // Instance is guaranteed to have the matching config
45
+ * return client.fetch(`*[_id == $id][0]`, { id: docId })
46
+ * }
47
+ * ```
48
+ *
49
+ * @example Match partial configuration
50
+ * ```tsx
51
+ * // Find an instance with specific auth configuration
52
+ * const instance = useSanityInstance({
53
+ * auth: { requireLogin: true }
54
+ * })
55
+ * ```
56
+ *
57
+ * @throws Error if no SanityInstance is found in context
58
+ * @throws Error if no matching instance is found for the provided config
15
59
  */
16
- export const useSanityInstance = (): SanityInstance => {
17
- const sanityInstance = useContext(SanityInstanceContext)
18
- if (!sanityInstance) {
19
- throw new Error('useSanityInstance must be called from within the SanityProvider')
60
+ export const useSanityInstance = (config?: SanityConfig): SanityInstance => {
61
+ const instance = useContext(SanityInstanceContext)
62
+
63
+ if (!instance) {
64
+ throw new Error(
65
+ `SanityInstance context not found. ${config ? `Requested config: ${JSON.stringify(config, null, 2)}. ` : ''}Please ensure that your component is wrapped in a <ResourceProvider> or a <SanityApp>.`,
66
+ )
67
+ }
68
+
69
+ if (!config) return instance
70
+
71
+ const match = instance.match(config)
72
+ if (!match) {
73
+ throw new Error(
74
+ `Could not find a matching Sanity instance for the requested configuration: ${JSON.stringify(config, null, 2)}.
75
+ Please ensure there is a <ResourceProvider> with a matching configuration in the component hierarchy.`,
76
+ )
20
77
  }
21
78
 
22
- return sanityInstance
79
+ return match
23
80
  }
@@ -0,0 +1,276 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {type DocumentHandle} from '@sanity/sdk'
3
+ import {act, renderHook} from '@testing-library/react'
4
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
5
+
6
+ import {useNavigateToStudioDocument} from './useNavigateToStudioDocument'
7
+
8
+ // Mock dependencies
9
+ const mockSendMessage = vi.fn()
10
+ const mockFetch = vi.fn()
11
+ let mockWorkspacesByProjectIdAndDataset = {}
12
+ let mockWorkspacesIsConnected = true
13
+ let mockStatusCallback: ((status: Status) => void) | null = null
14
+
15
+ vi.mock('../comlink/useWindowConnection', () => {
16
+ return {
17
+ useWindowConnection: ({onStatus}: {onStatus?: (status: Status) => void}) => {
18
+ mockStatusCallback = onStatus || null
19
+ return {
20
+ sendMessage: mockSendMessage,
21
+ fetch: mockFetch,
22
+ }
23
+ },
24
+ }
25
+ })
26
+
27
+ vi.mock('./useStudioWorkspacesByProjectIdDataset', () => {
28
+ return {
29
+ useStudioWorkspacesByProjectIdDataset: () => ({
30
+ workspacesByProjectIdAndDataset: mockWorkspacesByProjectIdAndDataset,
31
+ error: null,
32
+ isConnected: mockWorkspacesIsConnected,
33
+ }),
34
+ }
35
+ })
36
+
37
+ describe('useNavigateToStudioDocument', () => {
38
+ const mockDocumentHandle: DocumentHandle = {
39
+ documentId: 'doc123',
40
+ documentType: 'article',
41
+ projectId: 'project1',
42
+ dataset: 'dataset1',
43
+ }
44
+
45
+ const mockWorkspace = {
46
+ id: 'workspace123',
47
+ name: 'workspace1',
48
+ title: 'Workspace 1',
49
+ basePath: '/workspace1',
50
+ dataset: 'dataset1',
51
+ userApplicationId: 'user1',
52
+ url: 'https://test.sanity.studio',
53
+ }
54
+
55
+ beforeEach(() => {
56
+ vi.resetAllMocks()
57
+ mockWorkspacesByProjectIdAndDataset = {
58
+ 'project1:dataset1': [mockWorkspace],
59
+ }
60
+ mockWorkspacesIsConnected = true
61
+ mockStatusCallback = null
62
+ })
63
+
64
+ it('returns a function and connection status', () => {
65
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
66
+
67
+ // Initially not connected
68
+ expect(result.current.isConnected).toBe(false)
69
+
70
+ // Simulate connection
71
+ act(() => {
72
+ mockStatusCallback?.('connected')
73
+ })
74
+
75
+ expect(result.current).toEqual({
76
+ navigateToStudioDocument: expect.any(Function),
77
+ isConnected: true,
78
+ })
79
+ })
80
+
81
+ it('sends correct navigation message when called', () => {
82
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
83
+
84
+ // Simulate connection
85
+ act(() => {
86
+ mockStatusCallback?.('connected')
87
+ })
88
+
89
+ result.current.navigateToStudioDocument()
90
+
91
+ expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/bridge/navigate-to-resource', {
92
+ resourceId: 'workspace123',
93
+ resourceType: 'studio',
94
+ path: '/intent/edit/id=doc123;type=article',
95
+ })
96
+ })
97
+
98
+ it('does not send message when not connected', () => {
99
+ mockWorkspacesByProjectIdAndDataset = {}
100
+ mockWorkspacesIsConnected = false
101
+
102
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
103
+
104
+ // Simulate connection
105
+ act(() => {
106
+ mockStatusCallback?.('connected')
107
+ })
108
+
109
+ result.current.navigateToStudioDocument()
110
+
111
+ expect(mockSendMessage).not.toHaveBeenCalled()
112
+ })
113
+
114
+ it('does not send message when no workspace is found', () => {
115
+ mockWorkspacesByProjectIdAndDataset = {}
116
+ mockWorkspacesIsConnected = true
117
+
118
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
119
+
120
+ // Simulate connection
121
+ act(() => {
122
+ mockStatusCallback?.('connected')
123
+ })
124
+
125
+ result.current.navigateToStudioDocument()
126
+
127
+ expect(mockSendMessage).not.toHaveBeenCalled()
128
+ })
129
+
130
+ it('warns when multiple workspaces are found', () => {
131
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
132
+ const mockWorkspace2 = {...mockWorkspace, id: 'workspace2'}
133
+
134
+ mockWorkspacesByProjectIdAndDataset = {
135
+ 'project1:dataset1': [mockWorkspace, mockWorkspace2],
136
+ }
137
+
138
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
139
+
140
+ // Simulate connection
141
+ act(() => {
142
+ mockStatusCallback?.('connected')
143
+ })
144
+
145
+ result.current.navigateToStudioDocument()
146
+
147
+ expect(consoleSpy).toHaveBeenCalledWith(
148
+ 'Multiple workspaces found for document and no preferred studio url',
149
+ mockDocumentHandle,
150
+ )
151
+ expect(mockSendMessage).toHaveBeenCalledWith(
152
+ 'dashboard/v1/bridge/navigate-to-resource',
153
+ expect.objectContaining({
154
+ resourceId: mockWorkspace.id,
155
+ }),
156
+ )
157
+
158
+ consoleSpy.mockRestore()
159
+ })
160
+
161
+ it('warns and does not navigate when projectId or dataset is missing', () => {
162
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
163
+
164
+ const incompleteDocumentHandle: DocumentHandle = {
165
+ documentId: 'doc123',
166
+ documentType: 'article',
167
+ // missing projectId and dataset
168
+ }
169
+
170
+ const {result} = renderHook(() => useNavigateToStudioDocument(incompleteDocumentHandle))
171
+
172
+ // Simulate connection
173
+ act(() => {
174
+ mockStatusCallback?.('connected')
175
+ })
176
+
177
+ result.current.navigateToStudioDocument()
178
+
179
+ expect(consoleSpy).toHaveBeenCalledWith(
180
+ 'Project ID and dataset are required to navigate to a studio document',
181
+ )
182
+ expect(mockSendMessage).not.toHaveBeenCalled()
183
+
184
+ consoleSpy.mockRestore()
185
+ })
186
+
187
+ it('uses preferred studio URL when multiple workspaces are available', () => {
188
+ const preferredUrl = 'https://preferred.sanity.studio'
189
+ const mockWorkspace2 = {...mockWorkspace, id: 'workspace2', url: preferredUrl}
190
+
191
+ mockWorkspacesByProjectIdAndDataset = {
192
+ 'project1:dataset1': [mockWorkspace, mockWorkspace2],
193
+ }
194
+
195
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
196
+
197
+ // Simulate connection
198
+ act(() => {
199
+ mockStatusCallback?.('connected')
200
+ })
201
+
202
+ result.current.navigateToStudioDocument()
203
+
204
+ // Should choose workspace2 because it matches the preferred URL
205
+ expect(mockSendMessage).toHaveBeenCalledWith(
206
+ 'dashboard/v1/bridge/navigate-to-resource',
207
+ expect.objectContaining({
208
+ resourceId: 'workspace2',
209
+ }),
210
+ )
211
+ })
212
+
213
+ it('considers NO_PROJECT_ID:NO_DATASET workspaces when matching preferred URL', () => {
214
+ const preferredUrl = 'https://preferred.sanity.studio'
215
+ // Only have a workspace without projectId/dataset that matches the preferred URL
216
+ const mockWorkspaceNoProject = {
217
+ ...mockWorkspace,
218
+ id: 'workspace3',
219
+ url: preferredUrl,
220
+ projectId: undefined,
221
+ dataset: undefined,
222
+ }
223
+
224
+ mockWorkspacesByProjectIdAndDataset = {
225
+ 'NO_PROJECT_ID:NO_DATASET': [mockWorkspaceNoProject],
226
+ }
227
+
228
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
229
+
230
+ // Simulate connection
231
+ act(() => {
232
+ mockStatusCallback?.('connected')
233
+ })
234
+
235
+ result.current.navigateToStudioDocument()
236
+
237
+ // Should choose the NO_PROJECT_ID:NO_DATASET workspace because it matches the preferred URL
238
+ expect(mockSendMessage).toHaveBeenCalledWith(
239
+ 'dashboard/v1/bridge/navigate-to-resource',
240
+ expect.objectContaining({
241
+ resourceId: 'workspace3',
242
+ }),
243
+ )
244
+ })
245
+
246
+ it('warns with preferred URL info when no matching workspace is found', () => {
247
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
248
+ const preferredUrl = 'https://nonexistent.sanity.studio'
249
+
250
+ // Set up workspaces that don't match the preferred URL
251
+ mockWorkspacesByProjectIdAndDataset = {
252
+ 'project1:dataset1': [
253
+ {
254
+ ...mockWorkspace,
255
+ url: 'https://different.sanity.studio',
256
+ },
257
+ ],
258
+ }
259
+
260
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
261
+
262
+ // Simulate connection
263
+ act(() => {
264
+ mockStatusCallback?.('connected')
265
+ })
266
+
267
+ result.current.navigateToStudioDocument()
268
+
269
+ expect(consoleSpy).toHaveBeenCalledWith(
270
+ `No workspace found for document with projectId: ${mockDocumentHandle.projectId} and dataset: ${mockDocumentHandle.dataset} or with preferred studio url: ${preferredUrl}`,
271
+ )
272
+ expect(mockSendMessage).not.toHaveBeenCalled()
273
+
274
+ consoleSpy.mockRestore()
275
+ })
276
+ })
@@ -0,0 +1,139 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {type Bridge, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
3
+ import {type DocumentHandle} from '@sanity/sdk'
4
+ import {useCallback, useState} from 'react'
5
+
6
+ import {useWindowConnection} from '../comlink/useWindowConnection'
7
+ import {
8
+ type DashboardResource,
9
+ useStudioWorkspacesByProjectIdDataset,
10
+ } from './useStudioWorkspacesByProjectIdDataset'
11
+
12
+ /**
13
+ * @public
14
+ * @category Types
15
+ */
16
+ export interface NavigateToStudioResult {
17
+ navigateToStudioDocument: () => void
18
+ isConnected: boolean
19
+ }
20
+
21
+ /**
22
+ * @public
23
+ *
24
+ * Hook that provides a function to navigate to a given document in its parent Studio.
25
+ *
26
+ * Uses the `projectId` and `dataset` properties of the {@link DocumentHandle} you provide to resolve the correct Studio.
27
+ * This will only work if you have deployed a studio with a workspace with this `projectId` / `dataset` combination.
28
+ *
29
+ * @remarks If you write your own Document Handle to pass to this hook (as opposed to a Document Handle generated by another hook),
30
+ * it must include values for `documentId`, `documentType`, `projectId`, and `dataset`.
31
+ *
32
+ * @category Documents
33
+ * @param documentHandle - The document handle for the document to navigate to
34
+ * @param preferredStudioUrl - The preferred studio url to navigate to if you have multiple
35
+ * studios with the same projectId and dataset
36
+ * @returns An object containing:
37
+ * - `navigateToStudioDocument` - Function that when called will navigate to the studio document
38
+ * - `isConnected` - Boolean indicating if connection to Dashboard is established
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import {useNavigateToStudioDocument, type DocumentHandle} from '@sanity/sdk-react'
43
+ *
44
+ * function MyComponent({documentHandle}: {documentHandle: DocumentHandle}) {
45
+ * const {navigateToStudioDocument, isConnected} = useNavigateToStudioDocument(documentHandle)
46
+ *
47
+ * return (
48
+ * <button onClick={navigateToStudioDocument} disabled={!isConnected}>
49
+ * Navigate to Studio Document
50
+ * </button>
51
+ * )
52
+ * }
53
+ * ```
54
+ */
55
+ export function useNavigateToStudioDocument(
56
+ documentHandle: DocumentHandle,
57
+ preferredStudioUrl?: string,
58
+ ): NavigateToStudioResult {
59
+ const {workspacesByProjectIdAndDataset, isConnected: workspacesConnected} =
60
+ useStudioWorkspacesByProjectIdDataset()
61
+ const [status, setStatus] = useState<Status>('idle')
62
+ const {sendMessage} = useWindowConnection<Bridge.Navigation.NavigateToResourceMessage, never>({
63
+ name: SDK_NODE_NAME,
64
+ connectTo: SDK_CHANNEL_NAME,
65
+ onStatus: setStatus,
66
+ })
67
+
68
+ const navigateToStudioDocument = useCallback(() => {
69
+ const {projectId, dataset} = documentHandle
70
+
71
+ if (!workspacesConnected || status !== 'connected') {
72
+ // eslint-disable-next-line no-console
73
+ console.warn('Not connected to Dashboard')
74
+ return
75
+ }
76
+
77
+ if (!projectId || !dataset) {
78
+ // eslint-disable-next-line no-console
79
+ console.warn('Project ID and dataset are required to navigate to a studio document')
80
+ return
81
+ }
82
+
83
+ let workspace: DashboardResource | undefined
84
+
85
+ if (preferredStudioUrl) {
86
+ // Get workspaces matching the projectId:dataset and any workspaces without projectId/dataset,
87
+ // in case there hasn't been a manifest loaded yet
88
+ const allWorkspaces = [
89
+ ...(workspacesByProjectIdAndDataset[`${projectId}:${dataset}`] || []),
90
+ ...(workspacesByProjectIdAndDataset['NO_PROJECT_ID:NO_DATASET'] || []),
91
+ ]
92
+ workspace = allWorkspaces.find((w) => w.url === preferredStudioUrl)
93
+ } else {
94
+ const workspaces = workspacesByProjectIdAndDataset[`${projectId}:${dataset}`]
95
+ if (workspaces?.length > 1) {
96
+ // eslint-disable-next-line no-console
97
+ console.warn(
98
+ 'Multiple workspaces found for document and no preferred studio url',
99
+ documentHandle,
100
+ )
101
+ // eslint-disable-next-line no-console
102
+ console.warn('Using the first one', workspaces[0])
103
+ }
104
+
105
+ workspace = workspaces?.[0]
106
+ }
107
+
108
+ if (!workspace) {
109
+ // eslint-disable-next-line no-console
110
+ console.warn(
111
+ `No workspace found for document with projectId: ${projectId} and dataset: ${dataset}${preferredStudioUrl ? ` or with preferred studio url: ${preferredStudioUrl}` : ''}`,
112
+ )
113
+ return
114
+ }
115
+
116
+ const message: Bridge.Navigation.NavigateToResourceMessage = {
117
+ type: 'dashboard/v1/bridge/navigate-to-resource',
118
+ data: {
119
+ resourceId: workspace.id,
120
+ resourceType: 'studio',
121
+ path: `/intent/edit/id=${documentHandle.documentId};type=${documentHandle.documentType}`,
122
+ },
123
+ }
124
+
125
+ sendMessage(message.type, message.data)
126
+ }, [
127
+ documentHandle,
128
+ workspacesConnected,
129
+ status,
130
+ workspacesByProjectIdAndDataset,
131
+ sendMessage,
132
+ preferredStudioUrl,
133
+ ])
134
+
135
+ return {
136
+ navigateToStudioDocument,
137
+ isConnected: workspacesConnected && status === 'connected',
138
+ }
139
+ }