@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.
- package/README.md +6 -100
- package/dist/index.d.ts +2390 -2
- package/dist/index.js +1119 -2
- package/dist/index.js.map +1 -1
- package/package.json +35 -49
- package/src/_exports/index.ts +2 -10
- package/src/_exports/sdk-react.ts +73 -0
- package/src/components/SDKProvider.test.tsx +103 -0
- package/src/components/SDKProvider.tsx +52 -0
- package/src/components/SanityApp.test.tsx +244 -0
- package/src/components/SanityApp.tsx +106 -0
- package/src/components/auth/AuthBoundary.test.tsx +204 -29
- package/src/components/auth/AuthBoundary.tsx +96 -19
- package/src/components/auth/ConfigurationError.ts +22 -0
- package/src/components/auth/LoginCallback.test.tsx +22 -24
- package/src/components/auth/LoginCallback.tsx +6 -16
- package/src/components/auth/LoginError.test.tsx +11 -18
- package/src/components/auth/LoginError.tsx +43 -25
- package/src/components/utils.ts +22 -0
- package/src/context/ResourceProvider.test.tsx +157 -0
- package/src/context/ResourceProvider.tsx +111 -0
- package/src/context/SanityInstanceContext.ts +4 -0
- package/src/hooks/_synchronous-groq-js.mjs +4 -0
- package/src/hooks/auth/useAuthState.tsx +4 -5
- package/src/hooks/auth/useAuthToken.tsx +1 -1
- package/src/hooks/auth/useCurrentUser.tsx +28 -4
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
- package/src/hooks/auth/useDashboardOrganizationId.tsx +30 -0
- package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
- package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
- package/src/hooks/auth/useLogOut.test.tsx +2 -2
- package/src/hooks/auth/useLogOut.tsx +1 -1
- package/src/hooks/auth/useLoginUrl.tsx +14 -0
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
- package/src/hooks/client/useClient.ts +13 -33
- package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
- package/src/hooks/comlink/useFrameConnection.ts +107 -0
- package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
- package/src/hooks/comlink/useManageFavorite.ts +210 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
- package/src/hooks/comlink/useWindowConnection.ts +123 -0
- package/src/hooks/context/useSanityInstance.test.tsx +157 -15
- package/src/hooks/context/useSanityInstance.ts +68 -11
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
- package/src/hooks/datasets/useDatasets.test.ts +80 -0
- package/src/hooks/datasets/useDatasets.ts +52 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
- package/src/hooks/document/useApplyDocumentActions.ts +124 -0
- package/src/hooks/document/useDocument.test.ts +118 -0
- package/src/hooks/document/useDocument.ts +212 -0
- package/src/hooks/document/useDocumentEvent.test.ts +62 -0
- package/src/hooks/document/useDocumentEvent.ts +94 -0
- package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
- package/src/hooks/document/useDocumentPermissions.ts +131 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
- package/src/hooks/document/useEditDocument.test.ts +196 -0
- package/src/hooks/document/useEditDocument.ts +314 -0
- package/src/hooks/documents/useDocuments.test.tsx +179 -0
- package/src/hooks/documents/useDocuments.ts +300 -0
- package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
- package/src/hooks/helpers/createCallbackHook.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
- package/src/hooks/preview/usePreview.test.tsx +85 -17
- package/src/hooks/preview/usePreview.tsx +81 -22
- package/src/hooks/projection/useProjection.test.tsx +283 -0
- package/src/hooks/projection/useProjection.ts +232 -0
- package/src/hooks/projects/useProject.test.ts +80 -0
- package/src/hooks/projects/useProject.ts +51 -0
- package/src/hooks/projects/useProjects.test.ts +77 -0
- package/src/hooks/projects/useProjects.ts +45 -0
- package/src/hooks/query/useQuery.test.tsx +188 -0
- package/src/hooks/query/useQuery.ts +193 -0
- package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
- package/src/hooks/releases/useActiveReleases.ts +39 -0
- package/src/hooks/releases/usePerspective.test.tsx +120 -0
- package/src/hooks/releases/usePerspective.ts +49 -0
- package/src/hooks/users/useUsers.test.tsx +330 -0
- package/src/hooks/users/useUsers.ts +120 -0
- package/src/utils/getEnv.ts +21 -0
- package/src/version.ts +8 -0
- package/src/vite-env.d.ts +10 -0
- package/dist/_chunks-es/useLogOut.js +0 -44
- package/dist/_chunks-es/useLogOut.js.map +0 -1
- package/dist/assets/bundle-CcAyERuZ.css +0 -11
- package/dist/components.d.ts +0 -259
- package/dist/components.js +0 -301
- package/dist/components.js.map +0 -1
- package/dist/hooks.d.ts +0 -186
- package/dist/hooks.js +0 -81
- package/dist/hooks.js.map +0 -1
- package/src/_exports/components.ts +0 -13
- package/src/_exports/hooks.ts +0 -9
- package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
- package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
- package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
- package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
- package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
- package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
- package/src/components/Login/LoginLinks.test.tsx +0 -100
- package/src/components/Login/LoginLinks.tsx +0 -73
- package/src/components/auth/Login.test.tsx +0 -41
- package/src/components/auth/Login.tsx +0 -45
- package/src/components/auth/LoginFooter.test.tsx +0 -29
- package/src/components/auth/LoginFooter.tsx +0 -65
- package/src/components/auth/LoginLayout.test.tsx +0 -33
- package/src/components/auth/LoginLayout.tsx +0 -81
- package/src/components/context/SanityProvider.test.tsx +0 -25
- package/src/components/context/SanityProvider.tsx +0 -42
- package/src/css/css.config.js +0 -220
- package/src/css/paramour.css +0 -2347
- package/src/css/styles.css +0 -11
- package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
- package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
- package/src/hooks/auth/useLoginUrls.tsx +0 -51
- package/src/hooks/client/useClient.test.tsx +0 -130
- package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
- package/src/hooks/documentCollection/useDocuments.ts +0 -87
|
@@ -1,23 +1,80 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import {type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {useContext} from 'react'
|
|
3
3
|
|
|
4
|
-
import {SanityInstanceContext} from '../../
|
|
4
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Retrieves the current Sanity instance or finds a matching instance from the hierarchy
|
|
8
|
+
*
|
|
9
9
|
* @public
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
+
}
|