Skip to content

Commit 1d52a4f

Browse files
authored
chore(react): improve useNavigateToStudioDocument documentation and add tests (#356)
1 parent 6ca2ada commit 1d52a4f

File tree

2 files changed

+207
-3
lines changed

2 files changed

+207
-3
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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 mockWorkspacesByResourceId = {}
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('./useStudioWorkspacesByResourceId', () => {
28+
return {
29+
useStudioWorkspacesByResourceId: () => ({
30+
workspacesByResourceId: mockWorkspacesByResourceId,
31+
error: null,
32+
isConnected: mockWorkspacesIsConnected,
33+
}),
34+
}
35+
})
36+
37+
describe('useNavigateToStudioDocument', () => {
38+
const mockDocumentHandle: DocumentHandle = {
39+
_id: 'doc123',
40+
_type: 'article',
41+
resourceId: 'document:project1.dataset1:doc123',
42+
}
43+
44+
const mockWorkspace = {
45+
name: 'workspace1',
46+
title: 'Workspace 1',
47+
basePath: '/workspace1',
48+
dataset: 'dataset1',
49+
userApplicationId: 'user1',
50+
url: 'https://test.sanity.studio',
51+
_ref: 'workspace123',
52+
}
53+
54+
beforeEach(() => {
55+
vi.resetAllMocks()
56+
mockWorkspacesByResourceId = {
57+
'project1:dataset1': [mockWorkspace],
58+
}
59+
mockWorkspacesIsConnected = true
60+
mockStatusCallback = null
61+
})
62+
63+
it('returns a function and connection status', () => {
64+
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
65+
66+
// Initially not connected
67+
expect(result.current.isConnected).toBe(false)
68+
69+
// Simulate connection
70+
act(() => {
71+
mockStatusCallback?.('connected')
72+
})
73+
74+
expect(result.current).toEqual({
75+
navigateToStudioDocument: expect.any(Function),
76+
isConnected: true,
77+
})
78+
})
79+
80+
it('sends correct navigation message when called', () => {
81+
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
82+
83+
// Simulate connection
84+
act(() => {
85+
mockStatusCallback?.('connected')
86+
})
87+
88+
result.current.navigateToStudioDocument()
89+
90+
expect(mockSendMessage).toHaveBeenCalledWith('core/v1/bridge/navigate-to-resource', {
91+
resourceId: 'workspace123',
92+
resourceType: 'studio',
93+
path: '/intent/edit/id=doc123;type=article',
94+
})
95+
})
96+
97+
it('does not send message when not connected', () => {
98+
mockWorkspacesByResourceId = {}
99+
mockWorkspacesIsConnected = false
100+
101+
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
102+
103+
// Simulate connection
104+
act(() => {
105+
mockStatusCallback?.('connected')
106+
})
107+
108+
result.current.navigateToStudioDocument()
109+
110+
expect(mockSendMessage).not.toHaveBeenCalled()
111+
})
112+
113+
it('does not send message when no workspace is found', () => {
114+
mockWorkspacesByResourceId = {}
115+
mockWorkspacesIsConnected = true
116+
117+
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
118+
119+
// Simulate connection
120+
act(() => {
121+
mockStatusCallback?.('connected')
122+
})
123+
124+
result.current.navigateToStudioDocument()
125+
126+
expect(mockSendMessage).not.toHaveBeenCalled()
127+
})
128+
129+
it('handles invalid resourceId format', () => {
130+
const invalidDocHandle: DocumentHandle = {
131+
_id: 'doc123',
132+
_type: 'article',
133+
resourceId: 'document:project1.invalid:doc123' as `document:${string}.${string}:${string}`,
134+
}
135+
136+
const {result} = renderHook(() => useNavigateToStudioDocument(invalidDocHandle))
137+
138+
// Simulate connection
139+
act(() => {
140+
mockStatusCallback?.('connected')
141+
})
142+
143+
result.current.navigateToStudioDocument()
144+
145+
expect(mockSendMessage).not.toHaveBeenCalled()
146+
})
147+
148+
it('warns when multiple workspaces are found', () => {
149+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
150+
const mockWorkspace2 = {...mockWorkspace, _ref: 'workspace2'}
151+
152+
mockWorkspacesByResourceId = {
153+
'project1:dataset1': [mockWorkspace, mockWorkspace2],
154+
}
155+
156+
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
157+
158+
// Simulate connection
159+
act(() => {
160+
mockStatusCallback?.('connected')
161+
})
162+
163+
result.current.navigateToStudioDocument()
164+
165+
expect(consoleSpy).toHaveBeenCalledWith(
166+
'Multiple workspaces found for document',
167+
mockDocumentHandle.resourceId,
168+
)
169+
expect(mockSendMessage).toHaveBeenCalledWith(
170+
'core/v1/bridge/navigate-to-resource',
171+
expect.objectContaining({
172+
resourceId: mockWorkspace._ref,
173+
}),
174+
)
175+
176+
consoleSpy.mockRestore()
177+
})
178+
})

packages/react/src/hooks/dashboard/useNavigateToStudioDocument.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {type Status} from '@sanity/comlink'
2+
import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
23
import {type DocumentHandle} from '@sanity/sdk'
34
import {useCallback, useState} from 'react'
45

@@ -32,10 +33,35 @@ interface NavigateToStudioResult {
3233
/**
3334
* @public
3435
* Hook that provides a function to navigate to a studio document.
36+
* Currently, requires a document handle with a resourceId.
37+
* That resourceId is currently formatted like: `document:projectId.dataset:documentId`
38+
* If the hook you used to retrieve the document handle doesn't provide a resourceId like this,
39+
* you can construct it according to the above format with the document handle's _id.
40+
*
41+
* This will only work if you have deployed a studio with a workspace
42+
* with this projectId / dataset combination.
43+
* It may be able to take a custom URL in the future.
44+
*
45+
* This will likely change in the future.
3546
* @param documentHandle - The document handle containing document ID, type, and resource ID
3647
* @returns An object containing:
3748
* - navigateToStudioDocument - Function that when called will navigate to the studio document
38-
* - isConnected - Boolean indicating if connection to Core UI is established
49+
* - isConnected - Boolean indicating if connection to Dashboard is established
50+
*
51+
* @example
52+
* ```ts
53+
* import {navigateToStudioDocument, type DocumentHandle} from '@sanity/sdk'
54+
*
55+
* function MyComponent({documentHandle}: {documentHandle: DocumentHandle}) {
56+
* const {navigateToStudioDocument, isConnected} = useNavigateToStudioDocument(documentHandle)
57+
*
58+
* return (
59+
* <button onClick={navigateToStudioDocument} disabled={!isConnected}>
60+
* Navigate to Studio Document
61+
* </button>
62+
* )
63+
* }
64+
* ```
3965
*/
4066
export function useNavigateToStudioDocument(
4167
documentHandle: DocumentHandle,
@@ -44,8 +70,8 @@ export function useNavigateToStudioDocument(
4470
useStudioWorkspacesByResourceId()
4571
const [status, setStatus] = useState<Status>('idle')
4672
const {sendMessage} = useWindowConnection<NavigateToResourceMessage, never>({
47-
name: 'core/nodes/sdk',
48-
connectTo: 'core/channels/sdk',
73+
name: SDK_NODE_NAME,
74+
connectTo: SDK_CHANNEL_NAME,
4975
onStatus: setStatus,
5076
})
5177

0 commit comments

Comments
 (0)