@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
@@ -1,8 +1,8 @@
1
- import {type Message, type Node} from '@sanity/comlink'
1
+ import {type Message, type Node, type Status} from '@sanity/comlink'
2
2
  import {getOrCreateNode, releaseNode} from '@sanity/sdk'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
- import {renderHook} from '../../../test/test-utils'
5
+ import {act, renderHook} from '../../../test/test-utils'
6
6
  import {useWindowConnection} from './useWindowConnection'
7
7
 
8
8
  vi.mock(import('@sanity/sdk'), async (importOriginal) => {
@@ -26,24 +26,65 @@ interface AnotherMessage {
26
26
 
27
27
  type TestMessages = TestMessage | AnotherMessage
28
28
 
29
- function createMockNode() {
30
- return {
31
- // return unsubscribe function
32
- on: vi.fn(() => () => {}),
33
- post: vi.fn(),
34
- stop: vi.fn(),
35
- } as unknown as Node<Message, Message>
36
- }
37
-
38
29
  describe('useWindowConnection', () => {
39
30
  let node: Node<Message, Message>
31
+ let statusCallback: ((status: Status) => void) | null = null
32
+
33
+ function createMockNode() {
34
+ return {
35
+ // return unsubscribe function
36
+ on: vi.fn(() => () => {}),
37
+ post: vi.fn(),
38
+ stop: vi.fn(),
39
+ onStatus: vi.fn((callback) => {
40
+ statusCallback = callback
41
+ return () => {}
42
+ }),
43
+ } as unknown as Node<Message, Message>
44
+ }
40
45
 
41
46
  beforeEach(() => {
47
+ statusCallback = null
42
48
  node = createMockNode()
43
-
44
49
  vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
45
50
  })
46
51
 
52
+ it('should call onStatus callback when status changes', () => {
53
+ const onStatusMock = vi.fn()
54
+ renderHook(() =>
55
+ useWindowConnection<TestMessages, TestMessages>({
56
+ name: 'test',
57
+ connectTo: 'window',
58
+ onStatus: onStatusMock,
59
+ }),
60
+ )
61
+
62
+ act(() => {
63
+ statusCallback?.('connected')
64
+ })
65
+ expect(onStatusMock).toHaveBeenCalledWith('connected')
66
+
67
+ act(() => {
68
+ statusCallback?.('disconnected')
69
+ })
70
+ expect(onStatusMock).toHaveBeenCalledWith('disconnected')
71
+ })
72
+
73
+ it('should not throw if onStatus is not provided', () => {
74
+ renderHook(() =>
75
+ useWindowConnection<TestMessages, TestMessages>({
76
+ name: 'test',
77
+ connectTo: 'window',
78
+ }),
79
+ )
80
+
81
+ expect(() => {
82
+ act(() => {
83
+ statusCallback?.('connected')
84
+ })
85
+ }).not.toThrow()
86
+ })
87
+
47
88
  it('should register message handlers', () => {
48
89
  const mockHandler = vi.fn()
49
90
  const mockData = {someData: 'test'}
@@ -1,5 +1,6 @@
1
+ import {type MessageData, type Node, type Status} from '@sanity/comlink'
1
2
  import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
2
- import {useCallback, useEffect, useMemo} from 'react'
3
+ import {useCallback, useEffect, useRef} from 'react'
3
4
 
4
5
  import {useSanityInstance} from '../context/useSanityInstance'
5
6
 
@@ -17,6 +18,7 @@ export interface UseWindowConnectionOptions<TMessage extends FrameMessage> {
17
18
  name: string
18
19
  connectTo: string
19
20
  onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>
21
+ onStatus?: (status: Status) => void
20
22
  }
21
23
 
22
24
  /**
@@ -27,56 +29,94 @@ export interface WindowConnection<TMessage extends WindowMessage> {
27
29
  type: TType,
28
30
  data?: Extract<TMessage, {type: TType}>['data'],
29
31
  ) => void
32
+ fetch: <TResponse>(
33
+ type: string,
34
+ data?: MessageData,
35
+ options?: {
36
+ signal?: AbortSignal
37
+ suppressWarnings?: boolean
38
+ responseTimeout?: number
39
+ },
40
+ ) => Promise<TResponse>
30
41
  }
31
42
 
32
43
  /**
33
44
  * @internal
45
+ * Hook to wrap a Comlink node in a React hook.
46
+ * Our store functionality takes care of the lifecycle of the node,
47
+ * as well as sharing a single node between invocations if they share the same name.
48
+ *
49
+ * Generally not to be used directly, but to be used as a dependency of
50
+ * Comlink-powered hooks like `useManageFavorite`.
34
51
  */
35
52
  export function useWindowConnection<
36
53
  TWindowMessage extends WindowMessage,
37
54
  TFrameMessage extends FrameMessage,
38
- >(options: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {
39
- const {name, onMessage, connectTo} = options
55
+ >({
56
+ name,
57
+ connectTo,
58
+ onMessage,
59
+ onStatus,
60
+ }: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {
61
+ const nodeRef = useRef<Node<TWindowMessage, TFrameMessage> | null>(null)
62
+ const messageUnsubscribers = useRef<(() => void)[]>([])
40
63
  const instance = useSanityInstance()
41
64
 
42
- const node = useMemo(
43
- () => getOrCreateNode(instance, {name, connectTo}),
44
- [instance, name, connectTo],
45
- )
46
-
47
65
  useEffect(() => {
48
- if (!onMessage) return
49
-
50
- const unsubscribers: Array<() => void> = []
66
+ // the type cast is unfortunate, but the generic type of the node is not known here.
67
+ // We know that the node is a WindowMessage node, but not the generic types.
68
+ const node = getOrCreateNode(instance, {
69
+ name,
70
+ connectTo,
71
+ }) as unknown as Node<TWindowMessage, TFrameMessage>
72
+ nodeRef.current = node
51
73
 
52
- Object.entries(onMessage).forEach(([type, handler]) => {
53
- const unsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)
54
- unsubscribers.push(unsubscribe)
74
+ const statusUnsubscribe = node.onStatus((eventStatus) => {
75
+ onStatus?.(eventStatus)
55
76
  })
56
77
 
78
+ if (onMessage) {
79
+ Object.entries(onMessage).forEach(([type, handler]) => {
80
+ const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)
81
+ messageUnsubscribers.current.push(messageUnsubscribe)
82
+ })
83
+ }
84
+
57
85
  return () => {
58
- unsubscribers.forEach((unsub) => unsub())
86
+ statusUnsubscribe()
87
+ messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())
88
+ messageUnsubscribers.current = []
89
+ releaseNode(instance, name)
90
+ nodeRef.current = null
59
91
  }
60
- }, [node, onMessage])
92
+ }, [instance, name, connectTo, onMessage, onStatus])
61
93
 
62
94
  const sendMessage = useCallback(
63
- <TType extends WindowMessage['type']>(
64
- type: TType,
65
- data?: Extract<WindowMessage, {type: TType}>['data'],
66
- ) => {
67
- node?.post(type, data)
95
+ (type: TWindowMessage['type'], data?: Extract<TWindowMessage, {type: typeof type}>['data']) => {
96
+ if (!nodeRef.current) {
97
+ throw new Error('Cannot send message before connection is established')
98
+ }
99
+ nodeRef.current.post(type, data)
68
100
  },
69
- [node],
101
+ [],
70
102
  )
71
103
 
72
- // cleanup node on unmount
73
- useEffect(() => {
74
- return () => {
75
- releaseNode(instance, name)
76
- }
77
- }, [instance, name])
78
-
104
+ const fetch = useCallback(
105
+ <TResponse>(
106
+ type: string,
107
+ data?: MessageData,
108
+ fetchOptions?: {
109
+ responseTimeout?: number
110
+ signal?: AbortSignal
111
+ suppressWarnings?: boolean
112
+ },
113
+ ): Promise<TResponse> => {
114
+ return nodeRef.current?.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>
115
+ },
116
+ [],
117
+ )
79
118
  return {
80
119
  sendMessage,
120
+ fetch,
81
121
  }
82
122
  }
@@ -11,7 +11,7 @@ describe('useSanityInstance', () => {
11
11
 
12
12
  it('returns sanity instance when used within provider', () => {
13
13
  const wrapper = ({children}: {children: React.ReactNode}) => (
14
- <SanityProvider sanityInstance={sanityInstance}>{children}</SanityProvider>
14
+ <SanityProvider sanityInstances={[sanityInstance]}>{children}</SanityProvider>
15
15
  )
16
16
 
17
17
  const {result} = renderHook(() => useSanityInstance(), {wrapper})
@@ -1,23 +1,39 @@
1
1
  import {type SanityInstance} from '@sanity/sdk'
2
2
  import {useContext} from 'react'
3
3
 
4
- import {SanityInstanceContext} from '../../context/SanityProvider'
4
+ import {SanityInstanceContext} from '../../context/SanityInstanceContext'
5
5
 
6
6
  /**
7
7
  * `useSanityInstance` returns the current Sanity instance from the application context.
8
8
  * This must be called from within a `SanityProvider` component.
9
- * @public
9
+ * @internal
10
+ *
11
+ * @param resourceId - The resourceId of the Sanity instance to return (optional)
10
12
  * @returns The current Sanity instance
11
13
  * @example
12
14
  * ```tsx
13
- * const instance = useSanityInstance()
15
+ * const instance = useSanityInstance('abc123.production')
14
16
  * ```
15
17
  */
16
- export const useSanityInstance = (): SanityInstance => {
18
+ export const useSanityInstance = (resourceId?: string): SanityInstance => {
17
19
  const sanityInstance = useContext(SanityInstanceContext)
18
20
  if (!sanityInstance) {
19
21
  throw new Error('useSanityInstance must be called from within the SanityProvider')
20
22
  }
23
+ if (sanityInstance.length === 0) {
24
+ throw new Error('No Sanity instances found')
25
+ }
26
+ if (sanityInstance.length === 1 || !resourceId) {
27
+ return sanityInstance[0]
28
+ }
21
29
 
22
- return sanityInstance
30
+ if (!resourceId) {
31
+ throw new Error('resourceId is required when there are multiple Sanity instances')
32
+ }
33
+
34
+ const instance = sanityInstance.find((inst) => inst.identity.resourceId === resourceId)
35
+ if (!instance) {
36
+ throw new Error(`Sanity instance with resourceId ${resourceId} not found`)
37
+ }
38
+ return instance
23
39
  }
@@ -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('dashboard/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
+ 'dashboard/v1/bridge/navigate-to-resource',
171
+ expect.objectContaining({
172
+ resourceId: mockWorkspace._ref,
173
+ }),
174
+ )
175
+
176
+ consoleSpy.mockRestore()
177
+ })
178
+ })
@@ -0,0 +1,123 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {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 {useStudioWorkspacesByResourceId} from './useStudioWorkspacesByResourceId'
8
+
9
+ interface NavigateToResourceMessage {
10
+ type: 'dashboard/v1/bridge/navigate-to-resource'
11
+ data: {
12
+ /**
13
+ * Resource ID
14
+ */
15
+ resourceId: string
16
+ /**
17
+ * Resource type
18
+ * @example 'application' | 'studio'
19
+ */
20
+ resourceType: string
21
+ /**
22
+ * Path within the resource to navigate to.
23
+ */
24
+ path?: string
25
+ }
26
+ }
27
+
28
+ interface NavigateToStudioResult {
29
+ navigateToStudioDocument: () => void
30
+ isConnected: boolean
31
+ }
32
+
33
+ /**
34
+ * @public
35
+ * 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.
46
+ * @param documentHandle - The document handle containing document ID, type, and resource ID
47
+ * @returns An object containing:
48
+ * - navigateToStudioDocument - Function that when called will navigate to the studio document
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
+ * ```
65
+ */
66
+ export function useNavigateToStudioDocument(
67
+ documentHandle: DocumentHandle,
68
+ ): NavigateToStudioResult {
69
+ const {workspacesByResourceId, isConnected: workspacesConnected} =
70
+ useStudioWorkspacesByResourceId()
71
+ const [status, setStatus] = useState<Status>('idle')
72
+ const {sendMessage} = useWindowConnection<NavigateToResourceMessage, never>({
73
+ name: SDK_NODE_NAME,
74
+ connectTo: SDK_CHANNEL_NAME,
75
+ onStatus: setStatus,
76
+ })
77
+
78
+ const navigateToStudioDocument = useCallback(() => {
79
+ if (!workspacesConnected || status !== 'connected' || !documentHandle.resourceId) {
80
+ return
81
+ }
82
+
83
+ // Extract projectId and dataset from the resourceId (current format: document:projectId.dataset:documentId)
84
+ const [, projectAndDataset] = documentHandle.resourceId.split(':')
85
+ const [projectId, dataset] = projectAndDataset.split('.')
86
+ if (!projectId || !dataset) {
87
+ return
88
+ }
89
+
90
+ // Find the workspace for this document
91
+ const workspaces = workspacesByResourceId[`${projectId}:${dataset}`]
92
+ if (!workspaces?.length) {
93
+ // eslint-disable-next-line no-console
94
+ console.warn('No workspace found for document', documentHandle.resourceId)
95
+ return
96
+ }
97
+
98
+ if (workspaces.length > 1) {
99
+ // eslint-disable-next-line no-console
100
+ console.warn('Multiple workspaces found for document', documentHandle.resourceId)
101
+ // eslint-disable-next-line no-console
102
+ console.warn('Using the first one', workspaces[0])
103
+ }
104
+
105
+ const workspace = workspaces[0]
106
+
107
+ const message: NavigateToResourceMessage = {
108
+ type: 'dashboard/v1/bridge/navigate-to-resource',
109
+ data: {
110
+ resourceId: workspace._ref,
111
+ resourceType: 'studio',
112
+ path: `/intent/edit/id=${documentHandle._id};type=${documentHandle._type}`,
113
+ },
114
+ }
115
+
116
+ sendMessage(message.type, message.data)
117
+ }, [documentHandle, workspacesConnected, status, sendMessage, workspacesByResourceId])
118
+
119
+ return {
120
+ navigateToStudioDocument,
121
+ isConnected: workspacesConnected && status === 'connected',
122
+ }
123
+ }