@sanity/sdk-react 0.0.0-alpha.2 → 0.0.0-alpha.20

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 (112) hide show
  1. package/README.md +38 -67
  2. package/dist/index.d.ts +4742 -2
  3. package/dist/index.js +1054 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +27 -58
  6. package/src/_exports/index.ts +66 -10
  7. package/src/components/Login/LoginLinks.test.tsx +2 -12
  8. package/src/components/Login/LoginLinks.tsx +14 -29
  9. package/src/components/SDKProvider.test.tsx +79 -0
  10. package/src/components/SDKProvider.tsx +42 -0
  11. package/src/components/SanityApp.test.tsx +156 -0
  12. package/src/components/SanityApp.tsx +90 -0
  13. package/src/components/auth/AuthBoundary.test.tsx +4 -17
  14. package/src/components/auth/AuthBoundary.tsx +20 -4
  15. package/src/components/auth/Login.test.tsx +2 -16
  16. package/src/components/auth/Login.tsx +11 -30
  17. package/src/components/auth/LoginCallback.test.tsx +2 -17
  18. package/src/components/auth/LoginCallback.tsx +5 -10
  19. package/src/components/auth/LoginError.test.tsx +2 -17
  20. package/src/components/auth/LoginError.tsx +11 -16
  21. package/src/components/auth/LoginFooter.test.tsx +2 -16
  22. package/src/components/auth/LoginFooter.tsx +8 -24
  23. package/src/components/auth/LoginLayout.test.tsx +2 -16
  24. package/src/components/auth/LoginLayout.tsx +8 -38
  25. package/src/components/auth/authTestHelpers.tsx +11 -0
  26. package/src/components/utils.ts +22 -0
  27. package/src/context/SanityInstanceContext.ts +4 -0
  28. package/src/{components/context → context}/SanityProvider.test.tsx +2 -2
  29. package/src/context/SanityProvider.tsx +50 -0
  30. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  31. package/src/hooks/auth/useAuthState.tsx +4 -5
  32. package/src/hooks/auth/useAuthToken.tsx +1 -1
  33. package/src/hooks/auth/useCurrentUser.tsx +27 -4
  34. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  35. package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
  36. package/src/hooks/auth/useHandleCallback.tsx +1 -0
  37. package/src/hooks/auth/useLogOut.tsx +1 -1
  38. package/src/hooks/auth/useLoginUrls.tsx +1 -0
  39. package/src/hooks/client/useClient.ts +8 -30
  40. package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
  41. package/src/hooks/comlink/useFrameConnection.ts +107 -0
  42. package/src/hooks/comlink/useManageFavorite.test.ts +106 -0
  43. package/src/hooks/comlink/useManageFavorite.ts +101 -0
  44. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +77 -0
  45. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +79 -0
  46. package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
  47. package/src/hooks/comlink/useWindowConnection.ts +122 -0
  48. package/src/hooks/context/useSanityInstance.test.tsx +2 -2
  49. package/src/hooks/context/useSanityInstance.ts +24 -8
  50. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +97 -0
  51. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +274 -0
  52. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +91 -0
  53. package/src/hooks/datasets/useDatasets.ts +37 -0
  54. package/src/hooks/document/useApplyActions.test.ts +25 -0
  55. package/src/hooks/document/useApplyActions.ts +74 -0
  56. package/src/hooks/document/useDocument.test.ts +81 -0
  57. package/src/hooks/document/useDocument.ts +107 -0
  58. package/src/hooks/document/useDocumentEvent.test.ts +63 -0
  59. package/src/hooks/document/useDocumentEvent.ts +54 -0
  60. package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
  61. package/src/hooks/document/useDocumentSyncStatus.ts +30 -0
  62. package/src/hooks/document/useEditDocument.test.ts +179 -0
  63. package/src/hooks/document/useEditDocument.ts +195 -0
  64. package/src/hooks/document/usePermissions.ts +82 -0
  65. package/src/hooks/helpers/createCallbackHook.tsx +3 -2
  66. package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
  67. package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
  68. package/src/hooks/infiniteList/useInfiniteList.test.tsx +152 -0
  69. package/src/hooks/infiniteList/useInfiniteList.ts +174 -0
  70. package/src/hooks/paginatedList/usePaginatedList.test.tsx +259 -0
  71. package/src/hooks/paginatedList/usePaginatedList.ts +290 -0
  72. package/src/hooks/preview/usePreview.test.tsx +19 -10
  73. package/src/hooks/preview/usePreview.tsx +67 -13
  74. package/src/hooks/projection/useProjection.test.tsx +218 -0
  75. package/src/hooks/projection/useProjection.ts +147 -0
  76. package/src/hooks/projects/useProject.ts +45 -0
  77. package/src/hooks/projects/useProjects.ts +41 -0
  78. package/src/hooks/query/useQuery.test.tsx +188 -0
  79. package/src/hooks/query/useQuery.ts +103 -0
  80. package/src/hooks/users/useUsers.test.ts +163 -0
  81. package/src/hooks/users/useUsers.ts +107 -0
  82. package/src/utils/getEnv.ts +21 -0
  83. package/src/version.ts +8 -0
  84. package/src/vite-env.d.ts +10 -0
  85. package/dist/_chunks-es/useLogOut.js +0 -44
  86. package/dist/_chunks-es/useLogOut.js.map +0 -1
  87. package/dist/assets/bundle-CcAyERuZ.css +0 -11
  88. package/dist/components.d.ts +0 -257
  89. package/dist/components.js +0 -316
  90. package/dist/components.js.map +0 -1
  91. package/dist/hooks.d.ts +0 -187
  92. package/dist/hooks.js +0 -81
  93. package/dist/hooks.js.map +0 -1
  94. package/src/_exports/components.ts +0 -13
  95. package/src/_exports/hooks.ts +0 -9
  96. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
  97. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
  98. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
  99. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
  100. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
  101. package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
  102. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
  103. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
  104. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
  105. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
  106. package/src/components/context/SanityProvider.tsx +0 -42
  107. package/src/css/css.config.js +0 -220
  108. package/src/css/paramour.css +0 -2347
  109. package/src/css/styles.css +0 -11
  110. package/src/hooks/client/useClient.test.tsx +0 -130
  111. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  112. package/src/hooks/documentCollection/useDocuments.ts +0 -87
@@ -0,0 +1,79 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
3
+ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
4
+ import {useCallback, useState} from 'react'
5
+
6
+ import {useWindowConnection} from './useWindowConnection'
7
+
8
+ interface DocumentInteractionHistory {
9
+ recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
10
+ isConnected: boolean
11
+ }
12
+
13
+ /**
14
+ * @public
15
+ * Hook for managing document interaction history in Sanity Studio.
16
+ * This hook provides functionality to record document interactions.
17
+ * @category History
18
+ * @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
19
+ * @returns An object containing:
20
+ * - `recordEvent` - Function to record document interactions
21
+ * - `isConnected` - Boolean indicating if connection to Studio is established
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * function MyDocumentAction(props: DocumentActionProps) {
26
+ * const {_id, _type} = props
27
+ * const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
28
+ * _id,
29
+ * _type
30
+ * })
31
+ *
32
+ * return (
33
+ * <Button
34
+ * disabled={!isConnected}
35
+ * onClick={() => recordEvent('viewed')}
36
+ * text={'Viewed'}
37
+ * />
38
+ * )
39
+ * }
40
+ * ```
41
+ */
42
+ export function useRecordDocumentHistoryEvent({
43
+ _id,
44
+ _type,
45
+ }: DocumentHandle): DocumentInteractionHistory {
46
+ const [status, setStatus] = useState<Status>('idle')
47
+ const {sendMessage} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
48
+ name: SDK_NODE_NAME,
49
+ connectTo: SDK_CHANNEL_NAME,
50
+ onStatus: setStatus,
51
+ })
52
+
53
+ const recordEvent = useCallback(
54
+ (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
55
+ try {
56
+ const message: Events.HistoryMessage = {
57
+ type: 'core/v1/events/history',
58
+ data: {
59
+ eventType,
60
+ documentId: _id,
61
+ documentType: _type,
62
+ },
63
+ }
64
+
65
+ sendMessage(message.type, message.data)
66
+ } catch (error) {
67
+ // eslint-disable-next-line no-console
68
+ console.error('Failed to record history event:', error)
69
+ throw error
70
+ }
71
+ },
72
+ [_id, _type, sendMessage],
73
+ )
74
+
75
+ return {
76
+ recordEvent,
77
+ isConnected: status === 'connected',
78
+ }
79
+ }
@@ -0,0 +1,135 @@
1
+ import {type Message, type Node, type Status} from '@sanity/comlink'
2
+ import {getOrCreateNode, releaseNode} from '@sanity/sdk'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {act, renderHook} from '../../../test/test-utils'
6
+ import {useWindowConnection} from './useWindowConnection'
7
+
8
+ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
9
+ const actual = await importOriginal()
10
+ return {
11
+ ...actual,
12
+ getOrCreateNode: vi.fn(),
13
+ createNode: vi.fn(),
14
+ releaseNode: vi.fn(),
15
+ }
16
+ })
17
+ interface TestMessage {
18
+ type: 'TEST_MESSAGE'
19
+ data: {someData: string}
20
+ }
21
+
22
+ interface AnotherMessage {
23
+ type: 'ANOTHER_MESSAGE'
24
+ data: {otherData: number}
25
+ }
26
+
27
+ type TestMessages = TestMessage | AnotherMessage
28
+
29
+ describe('useWindowConnection', () => {
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
+ }
45
+
46
+ beforeEach(() => {
47
+ statusCallback = null
48
+ node = createMockNode()
49
+ vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
50
+ })
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
+
88
+ it('should register message handlers', () => {
89
+ const mockHandler = vi.fn()
90
+ const mockData = {someData: 'test'}
91
+
92
+ renderHook(() =>
93
+ useWindowConnection<TestMessages, TestMessages>({
94
+ name: 'test',
95
+ connectTo: 'window',
96
+ onMessage: {
97
+ TEST_MESSAGE: mockHandler,
98
+ ANOTHER_MESSAGE: vi.fn(),
99
+ },
100
+ }),
101
+ )
102
+
103
+ const onCallback = vi.mocked(node.on).mock.calls[0][1]
104
+ onCallback(mockData)
105
+
106
+ expect(mockHandler).toHaveBeenCalledWith(mockData)
107
+ })
108
+
109
+ it('should send messages through the node', () => {
110
+ const {result} = renderHook(() =>
111
+ useWindowConnection<TestMessages, TestMessages>({
112
+ name: 'test',
113
+ connectTo: 'window',
114
+ }),
115
+ )
116
+
117
+ result.current.sendMessage('TEST_MESSAGE', {someData: 'test'})
118
+ expect(node.post).toHaveBeenCalledWith('TEST_MESSAGE', {someData: 'test'})
119
+
120
+ result.current.sendMessage('ANOTHER_MESSAGE', {otherData: 123})
121
+ expect(node.post).toHaveBeenCalledWith('ANOTHER_MESSAGE', {otherData: 123})
122
+ })
123
+
124
+ it('should cleanup on unmount', () => {
125
+ const {unmount} = renderHook(() =>
126
+ useWindowConnection<TestMessages, TestMessages>({
127
+ name: 'test',
128
+ connectTo: 'window',
129
+ }),
130
+ )
131
+
132
+ unmount()
133
+ expect(releaseNode).toHaveBeenCalled()
134
+ })
135
+ })
@@ -0,0 +1,122 @@
1
+ import {type MessageData, type Node, type Status} from '@sanity/comlink'
2
+ import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
3
+ import {useCallback, useEffect, useRef} from 'react'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+
7
+ /**
8
+ * @internal
9
+ */
10
+ export type WindowMessageHandler<TFrameMessage extends FrameMessage> = (
11
+ event: TFrameMessage['data'],
12
+ ) => TFrameMessage['response']
13
+
14
+ /**
15
+ * @internal
16
+ */
17
+ export interface UseWindowConnectionOptions<TMessage extends FrameMessage> {
18
+ name: string
19
+ connectTo: string
20
+ onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>
21
+ onStatus?: (status: Status) => void
22
+ }
23
+
24
+ /**
25
+ * @internal
26
+ */
27
+ export interface WindowConnection<TMessage extends WindowMessage> {
28
+ sendMessage: <TType extends TMessage['type']>(
29
+ type: TType,
30
+ data?: Extract<TMessage, {type: TType}>['data'],
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>
41
+ }
42
+
43
+ /**
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`.
51
+ */
52
+ export function useWindowConnection<
53
+ TWindowMessage extends WindowMessage,
54
+ TFrameMessage extends FrameMessage,
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)[]>([])
63
+ const instance = useSanityInstance()
64
+
65
+ useEffect(() => {
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
73
+
74
+ const statusUnsubscribe = node.onStatus((eventStatus) => {
75
+ onStatus?.(eventStatus)
76
+ })
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
+
85
+ return () => {
86
+ statusUnsubscribe()
87
+ messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())
88
+ messageUnsubscribers.current = []
89
+ releaseNode(instance, name)
90
+ nodeRef.current = null
91
+ }
92
+ }, [instance, name, connectTo, onMessage, onStatus])
93
+
94
+ const sendMessage = useCallback(
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)
100
+ },
101
+ [],
102
+ )
103
+
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
+ )
118
+ return {
119
+ sendMessage,
120
+ fetch,
121
+ }
122
+ }
@@ -3,7 +3,7 @@ import {renderHook} from '@testing-library/react'
3
3
  import React from 'react'
4
4
  import {describe, expect, it, vi} from 'vitest'
5
5
 
6
- import {SanityProvider} from '../../components/context/SanityProvider'
6
+ import {SanityProvider} from '../../context/SanityProvider'
7
7
  import {useSanityInstance} from './useSanityInstance'
8
8
 
9
9
  describe('useSanityInstance', () => {
@@ -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
- import type {SanityInstance} from '@sanity/sdk'
1
+ import {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.
7
+ * `useSanityInstance` returns the current Sanity instance from the application context.
8
8
  * This must be called from within a `SanityProvider` component.
9
- * @public
10
- * @returns the current Sanity instance
9
+ * @internal
10
+ *
11
+ * @param resourceId - The resourceId of the Sanity instance to return (optional)
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,97 @@
1
+ import {type Status} from '@sanity/comlink'
2
+ import {type DocumentHandle} from '@sanity/sdk'
3
+ import {useCallback, useState} from 'react'
4
+
5
+ import {useWindowConnection} from '../comlink/useWindowConnection'
6
+ import {useStudioWorkspacesByResourceId} from './useStudioWorkspacesByResourceId'
7
+
8
+ interface NavigateToResourceMessage {
9
+ type: 'core/v1/bridge/navigate-to-resource'
10
+ data: {
11
+ /**
12
+ * Resource ID
13
+ */
14
+ resourceId: string
15
+ /**
16
+ * Resource type
17
+ * @example 'application' | 'studio'
18
+ */
19
+ resourceType: string
20
+ /**
21
+ * Path within the resource to navigate to.
22
+ */
23
+ path?: string
24
+ }
25
+ }
26
+
27
+ interface NavigateToStudioResult {
28
+ navigateToStudioDocument: () => void
29
+ isConnected: boolean
30
+ }
31
+
32
+ /**
33
+ * @public
34
+ * Hook that provides a function to navigate to a studio document.
35
+ * @param documentHandle - The document handle containing document ID, type, and resource ID
36
+ * @returns An object containing:
37
+ * - navigateToStudioDocument - Function that when called will navigate to the studio document
38
+ * - isConnected - Boolean indicating if connection to Core UI is established
39
+ */
40
+ export function useNavigateToStudioDocument(
41
+ documentHandle: DocumentHandle,
42
+ ): NavigateToStudioResult {
43
+ const {workspacesByResourceId, isConnected: workspacesConnected} =
44
+ useStudioWorkspacesByResourceId()
45
+ const [status, setStatus] = useState<Status>('idle')
46
+ const {sendMessage} = useWindowConnection<NavigateToResourceMessage, never>({
47
+ name: 'core/nodes/sdk',
48
+ connectTo: 'core/channels/sdk',
49
+ onStatus: setStatus,
50
+ })
51
+
52
+ const navigateToStudioDocument = useCallback(() => {
53
+ if (!workspacesConnected || status !== 'connected' || !documentHandle.resourceId) {
54
+ return
55
+ }
56
+
57
+ // Extract projectId and dataset from the resourceId (current format: document:projectId.dataset:documentId)
58
+ const [, projectAndDataset] = documentHandle.resourceId.split(':')
59
+ const [projectId, dataset] = projectAndDataset.split('.')
60
+ if (!projectId || !dataset) {
61
+ return
62
+ }
63
+
64
+ // Find the workspace for this document
65
+ const workspaces = workspacesByResourceId[`${projectId}:${dataset}`]
66
+ if (!workspaces?.length) {
67
+ // eslint-disable-next-line no-console
68
+ console.warn('No workspace found for document', documentHandle.resourceId)
69
+ return
70
+ }
71
+
72
+ if (workspaces.length > 1) {
73
+ // eslint-disable-next-line no-console
74
+ console.warn('Multiple workspaces found for document', documentHandle.resourceId)
75
+ // eslint-disable-next-line no-console
76
+ console.warn('Using the first one', workspaces[0])
77
+ }
78
+
79
+ const workspace = workspaces[0]
80
+
81
+ const message: NavigateToResourceMessage = {
82
+ type: 'core/v1/bridge/navigate-to-resource',
83
+ data: {
84
+ resourceId: workspace._ref,
85
+ resourceType: 'studio',
86
+ path: `/intent/edit/id=${documentHandle._id};type=${documentHandle._type}`,
87
+ },
88
+ }
89
+
90
+ sendMessage(message.type, message.data)
91
+ }, [documentHandle, workspacesConnected, status, sendMessage, workspacesByResourceId])
92
+
93
+ return {
94
+ navigateToStudioDocument,
95
+ isConnected: workspacesConnected && status === 'connected',
96
+ }
97
+ }