@sanity/sdk-react 0.0.1 → 0.0.2
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/dist/index.d.ts +62 -14
- package/dist/index.js +112 -143
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/_exports/sdk-react.ts +2 -2
- package/src/hooks/comlink/useWindowConnection.test.tsx +145 -0
- package/src/hooks/comlink/useWindowConnection.ts +32 -30
- package/src/hooks/{comlink → dashboard}/useManageFavorite.test.ts +84 -134
- package/src/hooks/{comlink → dashboard}/useManageFavorite.ts +4 -39
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +2 -73
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +20 -27
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +69 -0
- package/src/hooks/{comlink → dashboard}/useRecordDocumentHistoryEvent.ts +17 -10
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +14 -85
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +33 -8
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +0 -85
- package/src/hooks/comlink/useWindowConnection.test.ts +0 -135
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {type Status} from '@sanity/comlink'
|
|
2
1
|
import {
|
|
3
2
|
type CanvasResource,
|
|
4
3
|
type Events,
|
|
@@ -14,16 +13,15 @@ import {
|
|
|
14
13
|
getFavoritesState,
|
|
15
14
|
resolveFavoritesState,
|
|
16
15
|
} from '@sanity/sdk'
|
|
17
|
-
import {useCallback, useMemo,
|
|
16
|
+
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
18
17
|
|
|
18
|
+
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
19
19
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
20
|
-
import {useWindowConnection} from './useWindowConnection'
|
|
21
20
|
|
|
22
21
|
interface ManageFavorite extends FavoriteStatusResponse {
|
|
23
22
|
favorite: () => Promise<void>
|
|
24
23
|
unfavorite: () => Promise<void>
|
|
25
24
|
isFavorited: boolean
|
|
26
|
-
isConnected: boolean
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
interface UseManageFavoriteProps extends DocumentHandle {
|
|
@@ -92,11 +90,9 @@ export function useManageFavorite({
|
|
|
92
90
|
resourceType,
|
|
93
91
|
schemaName,
|
|
94
92
|
}: UseManageFavoriteProps): ManageFavorite {
|
|
95
|
-
const [status, setStatus] = useState<Status>('idle')
|
|
96
93
|
const {fetch} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
|
|
97
94
|
name: SDK_NODE_NAME,
|
|
98
95
|
connectTo: SDK_CHANNEL_NAME,
|
|
99
|
-
onStatus: setStatus,
|
|
100
96
|
})
|
|
101
97
|
const instance = useSanityInstance()
|
|
102
98
|
const {config} = instance
|
|
@@ -136,7 +132,7 @@ export function useManageFavorite({
|
|
|
136
132
|
|
|
137
133
|
const handleFavoriteAction = useCallback(
|
|
138
134
|
async (action: 'added' | 'removed') => {
|
|
139
|
-
if (
|
|
135
|
+
if (!fetch || !documentId || !documentType || !resourceType) return
|
|
140
136
|
|
|
141
137
|
try {
|
|
142
138
|
const payload = {
|
|
@@ -165,46 +161,15 @@ export function useManageFavorite({
|
|
|
165
161
|
throw err
|
|
166
162
|
}
|
|
167
163
|
},
|
|
168
|
-
[
|
|
169
|
-
fetch,
|
|
170
|
-
documentId,
|
|
171
|
-
documentType,
|
|
172
|
-
resourceId,
|
|
173
|
-
resourceType,
|
|
174
|
-
schemaName,
|
|
175
|
-
instance,
|
|
176
|
-
context,
|
|
177
|
-
status,
|
|
178
|
-
],
|
|
164
|
+
[fetch, documentId, documentType, resourceId, resourceType, schemaName, instance, context],
|
|
179
165
|
)
|
|
180
166
|
|
|
181
167
|
const favorite = useCallback(() => handleFavoriteAction('added'), [handleFavoriteAction])
|
|
182
168
|
const unfavorite = useCallback(() => handleFavoriteAction('removed'), [handleFavoriteAction])
|
|
183
169
|
|
|
184
|
-
// if state is undefined, we should suspend
|
|
185
|
-
if (!state) {
|
|
186
|
-
try {
|
|
187
|
-
const promise = resolveFavoritesState(instance, context)
|
|
188
|
-
throw promise
|
|
189
|
-
} catch (err) {
|
|
190
|
-
// If we get a timeout error, return a fallback state instead of suspending
|
|
191
|
-
if (err instanceof Error && err.message === 'Favorites service connection timeout') {
|
|
192
|
-
return {
|
|
193
|
-
favorite: async () => {},
|
|
194
|
-
unfavorite: async () => {},
|
|
195
|
-
isFavorited: false,
|
|
196
|
-
isConnected: false,
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// For other errors, continue with suspension
|
|
200
|
-
throw err
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
170
|
return {
|
|
205
171
|
favorite,
|
|
206
172
|
unfavorite,
|
|
207
173
|
isFavorited,
|
|
208
|
-
isConnected: status === 'connected',
|
|
209
174
|
}
|
|
210
175
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {type Status} from '@sanity/comlink'
|
|
2
1
|
import {type DocumentHandle} from '@sanity/sdk'
|
|
3
|
-
import {
|
|
2
|
+
import {renderHook} from '@testing-library/react'
|
|
4
3
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
4
|
|
|
6
5
|
import {useNavigateToStudioDocument} from './useNavigateToStudioDocument'
|
|
@@ -9,13 +8,10 @@ import {useNavigateToStudioDocument} from './useNavigateToStudioDocument'
|
|
|
9
8
|
const mockSendMessage = vi.fn()
|
|
10
9
|
const mockFetch = vi.fn()
|
|
11
10
|
let mockWorkspacesByProjectIdAndDataset = {}
|
|
12
|
-
let mockWorkspacesIsConnected = true
|
|
13
|
-
let mockStatusCallback: ((status: Status) => void) | null = null
|
|
14
11
|
|
|
15
12
|
vi.mock('../comlink/useWindowConnection', () => {
|
|
16
13
|
return {
|
|
17
|
-
useWindowConnection: (
|
|
18
|
-
mockStatusCallback = onStatus || null
|
|
14
|
+
useWindowConnection: () => {
|
|
19
15
|
return {
|
|
20
16
|
sendMessage: mockSendMessage,
|
|
21
17
|
fetch: mockFetch,
|
|
@@ -29,7 +25,6 @@ vi.mock('./useStudioWorkspacesByProjectIdDataset', () => {
|
|
|
29
25
|
useStudioWorkspacesByProjectIdDataset: () => ({
|
|
30
26
|
workspacesByProjectIdAndDataset: mockWorkspacesByProjectIdAndDataset,
|
|
31
27
|
error: null,
|
|
32
|
-
isConnected: mockWorkspacesIsConnected,
|
|
33
28
|
}),
|
|
34
29
|
}
|
|
35
30
|
})
|
|
@@ -57,35 +52,19 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
57
52
|
mockWorkspacesByProjectIdAndDataset = {
|
|
58
53
|
'project1:dataset1': [mockWorkspace],
|
|
59
54
|
}
|
|
60
|
-
mockWorkspacesIsConnected = true
|
|
61
|
-
mockStatusCallback = null
|
|
62
55
|
})
|
|
63
56
|
|
|
64
57
|
it('returns a function and connection status', () => {
|
|
65
58
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
66
59
|
|
|
67
|
-
// Initially not connected
|
|
68
|
-
expect(result.current.isConnected).toBe(false)
|
|
69
|
-
|
|
70
|
-
// Simulate connection
|
|
71
|
-
act(() => {
|
|
72
|
-
mockStatusCallback?.('connected')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
60
|
expect(result.current).toEqual({
|
|
76
61
|
navigateToStudioDocument: expect.any(Function),
|
|
77
|
-
isConnected: true,
|
|
78
62
|
})
|
|
79
63
|
})
|
|
80
64
|
|
|
81
65
|
it('sends correct navigation message when called', () => {
|
|
82
66
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
83
67
|
|
|
84
|
-
// Simulate connection
|
|
85
|
-
act(() => {
|
|
86
|
-
mockStatusCallback?.('connected')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
68
|
result.current.navigateToStudioDocument()
|
|
90
69
|
|
|
91
70
|
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/bridge/navigate-to-resource', {
|
|
@@ -95,35 +74,10 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
95
74
|
})
|
|
96
75
|
})
|
|
97
76
|
|
|
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
77
|
it('does not send message when no workspace is found', () => {
|
|
115
78
|
mockWorkspacesByProjectIdAndDataset = {}
|
|
116
|
-
mockWorkspacesIsConnected = true
|
|
117
|
-
|
|
118
79
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
119
|
-
|
|
120
|
-
// Simulate connection
|
|
121
|
-
act(() => {
|
|
122
|
-
mockStatusCallback?.('connected')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
80
|
result.current.navigateToStudioDocument()
|
|
126
|
-
|
|
127
81
|
expect(mockSendMessage).not.toHaveBeenCalled()
|
|
128
82
|
})
|
|
129
83
|
|
|
@@ -137,11 +91,6 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
137
91
|
|
|
138
92
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
139
93
|
|
|
140
|
-
// Simulate connection
|
|
141
|
-
act(() => {
|
|
142
|
-
mockStatusCallback?.('connected')
|
|
143
|
-
})
|
|
144
|
-
|
|
145
94
|
result.current.navigateToStudioDocument()
|
|
146
95
|
|
|
147
96
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
@@ -169,11 +118,6 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
169
118
|
|
|
170
119
|
const {result} = renderHook(() => useNavigateToStudioDocument(incompleteDocumentHandle))
|
|
171
120
|
|
|
172
|
-
// Simulate connection
|
|
173
|
-
act(() => {
|
|
174
|
-
mockStatusCallback?.('connected')
|
|
175
|
-
})
|
|
176
|
-
|
|
177
121
|
result.current.navigateToStudioDocument()
|
|
178
122
|
|
|
179
123
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
@@ -194,11 +138,6 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
194
138
|
|
|
195
139
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
|
|
196
140
|
|
|
197
|
-
// Simulate connection
|
|
198
|
-
act(() => {
|
|
199
|
-
mockStatusCallback?.('connected')
|
|
200
|
-
})
|
|
201
|
-
|
|
202
141
|
result.current.navigateToStudioDocument()
|
|
203
142
|
|
|
204
143
|
// Should choose workspace2 because it matches the preferred URL
|
|
@@ -227,11 +166,6 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
227
166
|
|
|
228
167
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
|
|
229
168
|
|
|
230
|
-
// Simulate connection
|
|
231
|
-
act(() => {
|
|
232
|
-
mockStatusCallback?.('connected')
|
|
233
|
-
})
|
|
234
|
-
|
|
235
169
|
result.current.navigateToStudioDocument()
|
|
236
170
|
|
|
237
171
|
// Should choose the NO_PROJECT_ID:NO_DATASET workspace because it matches the preferred URL
|
|
@@ -259,11 +193,6 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
259
193
|
|
|
260
194
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
|
|
261
195
|
|
|
262
|
-
// Simulate connection
|
|
263
|
-
act(() => {
|
|
264
|
-
mockStatusCallback?.('connected')
|
|
265
|
-
})
|
|
266
|
-
|
|
267
196
|
result.current.navigateToStudioDocument()
|
|
268
197
|
|
|
269
198
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {type Status} from '@sanity/comlink'
|
|
2
1
|
import {type Bridge, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
3
2
|
import {type DocumentHandle} from '@sanity/sdk'
|
|
4
|
-
import {useCallback
|
|
3
|
+
import {useCallback} from 'react'
|
|
5
4
|
|
|
6
5
|
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
7
6
|
import {
|
|
@@ -15,7 +14,6 @@ import {
|
|
|
15
14
|
*/
|
|
16
15
|
export interface NavigateToStudioResult {
|
|
17
16
|
navigateToStudioDocument: () => void
|
|
18
|
-
isConnected: boolean
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
/**
|
|
@@ -38,16 +36,28 @@ export interface NavigateToStudioResult {
|
|
|
38
36
|
* - `isConnected` - Boolean indicating if connection to Dashboard is established
|
|
39
37
|
*
|
|
40
38
|
* @example
|
|
41
|
-
* ```
|
|
39
|
+
* ```tsx
|
|
42
40
|
* import {useNavigateToStudioDocument, type DocumentHandle} from '@sanity/sdk-react'
|
|
41
|
+
* import {Button} from '@sanity/ui'
|
|
42
|
+
* import {Suspense} from 'react'
|
|
43
43
|
*
|
|
44
|
-
* function
|
|
44
|
+
* function NavigateButton({documentHandle}: {documentHandle: DocumentHandle}) {
|
|
45
45
|
* const {navigateToStudioDocument, isConnected} = useNavigateToStudioDocument(documentHandle)
|
|
46
|
+
* return (
|
|
47
|
+
* <Button
|
|
48
|
+
* disabled={!isConnected}
|
|
49
|
+
* onClick={navigateToStudioDocument}
|
|
50
|
+
* text="Navigate to Studio Document"
|
|
51
|
+
* />
|
|
52
|
+
* )
|
|
53
|
+
* }
|
|
46
54
|
*
|
|
55
|
+
* // Wrap the component with Suspense since the hook may suspend
|
|
56
|
+
* function MyDocumentAction({documentHandle}: {documentHandle: DocumentHandle}) {
|
|
47
57
|
* return (
|
|
48
|
-
* <
|
|
49
|
-
*
|
|
50
|
-
* </
|
|
58
|
+
* <Suspense fallback={<Button text="Loading..." disabled />}>
|
|
59
|
+
* <NavigateButton documentHandle={documentHandle} />
|
|
60
|
+
* </Suspense>
|
|
51
61
|
* )
|
|
52
62
|
* }
|
|
53
63
|
* ```
|
|
@@ -56,24 +66,15 @@ export function useNavigateToStudioDocument(
|
|
|
56
66
|
documentHandle: DocumentHandle,
|
|
57
67
|
preferredStudioUrl?: string,
|
|
58
68
|
): NavigateToStudioResult {
|
|
59
|
-
const {workspacesByProjectIdAndDataset
|
|
60
|
-
useStudioWorkspacesByProjectIdDataset()
|
|
61
|
-
const [status, setStatus] = useState<Status>('idle')
|
|
69
|
+
const {workspacesByProjectIdAndDataset} = useStudioWorkspacesByProjectIdDataset()
|
|
62
70
|
const {sendMessage} = useWindowConnection<Bridge.Navigation.NavigateToResourceMessage, never>({
|
|
63
71
|
name: SDK_NODE_NAME,
|
|
64
72
|
connectTo: SDK_CHANNEL_NAME,
|
|
65
|
-
onStatus: setStatus,
|
|
66
73
|
})
|
|
67
74
|
|
|
68
75
|
const navigateToStudioDocument = useCallback(() => {
|
|
69
76
|
const {projectId, dataset} = documentHandle
|
|
70
77
|
|
|
71
|
-
if (!workspacesConnected || status !== 'connected') {
|
|
72
|
-
// eslint-disable-next-line no-console
|
|
73
|
-
console.warn('Not connected to Dashboard')
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
78
|
if (!projectId || !dataset) {
|
|
78
79
|
// eslint-disable-next-line no-console
|
|
79
80
|
console.warn('Project ID and dataset are required to navigate to a studio document')
|
|
@@ -123,17 +124,9 @@ export function useNavigateToStudioDocument(
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
sendMessage(message.type, message.data)
|
|
126
|
-
}, [
|
|
127
|
-
documentHandle,
|
|
128
|
-
workspacesConnected,
|
|
129
|
-
status,
|
|
130
|
-
workspacesByProjectIdAndDataset,
|
|
131
|
-
sendMessage,
|
|
132
|
-
preferredStudioUrl,
|
|
133
|
-
])
|
|
127
|
+
}, [documentHandle, workspacesByProjectIdAndDataset, sendMessage, preferredStudioUrl])
|
|
134
128
|
|
|
135
129
|
return {
|
|
136
130
|
navigateToStudioDocument,
|
|
137
|
-
isConnected: workspacesConnected && status === 'connected',
|
|
138
131
|
}
|
|
139
132
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {renderHook} from '../../../test/test-utils'
|
|
4
|
+
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
5
|
+
import {useRecordDocumentHistoryEvent} from './useRecordDocumentHistoryEvent'
|
|
6
|
+
|
|
7
|
+
vi.mock('../comlink/useWindowConnection', () => ({
|
|
8
|
+
useWindowConnection: vi.fn(),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
describe('useRecordDocumentHistoryEvent', () => {
|
|
12
|
+
let mockSendMessage = vi.fn()
|
|
13
|
+
const mockDocumentHandle = {
|
|
14
|
+
documentId: 'mock-id',
|
|
15
|
+
documentType: 'mock-type',
|
|
16
|
+
resourceType: 'studio' as const,
|
|
17
|
+
resourceId: 'mock-resource-id',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockSendMessage = vi.fn()
|
|
22
|
+
vi.mocked(useWindowConnection).mockImplementation(() => {
|
|
23
|
+
return {
|
|
24
|
+
sendMessage: mockSendMessage,
|
|
25
|
+
fetch: vi.fn(),
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should send correct message when recording events', () => {
|
|
31
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
32
|
+
|
|
33
|
+
result.current.recordEvent('viewed')
|
|
34
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/history', {
|
|
35
|
+
eventType: 'viewed',
|
|
36
|
+
document: {
|
|
37
|
+
id: 'mock-id',
|
|
38
|
+
type: 'mock-type',
|
|
39
|
+
resource: {
|
|
40
|
+
id: 'mock-resource-id',
|
|
41
|
+
type: 'studio',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should handle errors when sending messages', () => {
|
|
48
|
+
mockSendMessage.mockImplementation(() => {
|
|
49
|
+
throw new Error('Failed to send message')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
53
|
+
|
|
54
|
+
expect(() => result.current.recordEvent('viewed')).toThrow('Failed to send message')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should throw error when resourceId is missing for non-studio resources', () => {
|
|
58
|
+
const mockMediaDocumentHandle = {
|
|
59
|
+
documentId: 'mock-id',
|
|
60
|
+
documentType: 'mock-type',
|
|
61
|
+
resourceType: 'media-library' as const,
|
|
62
|
+
resourceId: undefined,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
expect(() => renderHook(() => useRecordDocumentHistoryEvent(mockMediaDocumentHandle))).toThrow(
|
|
66
|
+
'resourceId is required for media-library and canvas resources',
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {type Status} from '@sanity/comlink'
|
|
2
1
|
import {
|
|
3
2
|
type CanvasResource,
|
|
4
3
|
type Events,
|
|
@@ -8,13 +7,12 @@ import {
|
|
|
8
7
|
type StudioResource,
|
|
9
8
|
} from '@sanity/message-protocol'
|
|
10
9
|
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
11
|
-
import {useCallback
|
|
10
|
+
import {useCallback} from 'react'
|
|
12
11
|
|
|
13
|
-
import {useWindowConnection} from '
|
|
12
|
+
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
14
13
|
|
|
15
14
|
interface DocumentInteractionHistory {
|
|
16
15
|
recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
|
|
17
|
-
isConnected: boolean
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
/**
|
|
@@ -42,7 +40,11 @@ interface UseRecordDocumentHistoryEventProps extends DocumentHandle {
|
|
|
42
40
|
*
|
|
43
41
|
* @example
|
|
44
42
|
* ```tsx
|
|
45
|
-
*
|
|
43
|
+
* import {useRecordDocumentHistoryEvent} from '@sanity/sdk-react'
|
|
44
|
+
* import {Button} from '@sanity/ui'
|
|
45
|
+
* import {Suspense} from 'react'
|
|
46
|
+
*
|
|
47
|
+
* function RecordEventButton(props: DocumentActionProps) {
|
|
46
48
|
* const {documentId, documentType, resourceType, resourceId} = props
|
|
47
49
|
* const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
|
|
48
50
|
* documentId,
|
|
@@ -50,15 +52,23 @@ interface UseRecordDocumentHistoryEventProps extends DocumentHandle {
|
|
|
50
52
|
* resourceType,
|
|
51
53
|
* resourceId,
|
|
52
54
|
* })
|
|
53
|
-
*
|
|
54
55
|
* return (
|
|
55
56
|
* <Button
|
|
56
57
|
* disabled={!isConnected}
|
|
57
58
|
* onClick={() => recordEvent('viewed')}
|
|
58
|
-
* text=
|
|
59
|
+
* text="Viewed"
|
|
59
60
|
* />
|
|
60
61
|
* )
|
|
61
62
|
* }
|
|
63
|
+
*
|
|
64
|
+
* // Wrap the component with Suspense since the hook may suspend
|
|
65
|
+
* function MyDocumentAction(props: DocumentActionProps) {
|
|
66
|
+
* return (
|
|
67
|
+
* <Suspense fallback={<Button text="Loading..." disabled />}>
|
|
68
|
+
* <RecordEventButton {...props} />
|
|
69
|
+
* </Suspense>
|
|
70
|
+
* )
|
|
71
|
+
* }
|
|
62
72
|
* ```
|
|
63
73
|
*/
|
|
64
74
|
export function useRecordDocumentHistoryEvent({
|
|
@@ -68,11 +78,9 @@ export function useRecordDocumentHistoryEvent({
|
|
|
68
78
|
resourceId,
|
|
69
79
|
schemaName,
|
|
70
80
|
}: UseRecordDocumentHistoryEventProps): DocumentInteractionHistory {
|
|
71
|
-
const [status, setStatus] = useState<Status>('idle')
|
|
72
81
|
const {sendMessage} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
|
|
73
82
|
name: SDK_NODE_NAME,
|
|
74
83
|
connectTo: SDK_CHANNEL_NAME,
|
|
75
|
-
onStatus: setStatus,
|
|
76
84
|
})
|
|
77
85
|
|
|
78
86
|
if (resourceType !== 'studio' && !resourceId) {
|
|
@@ -110,6 +118,5 @@ export function useRecordDocumentHistoryEvent({
|
|
|
110
118
|
|
|
111
119
|
return {
|
|
112
120
|
recordEvent,
|
|
113
|
-
isConnected: status === 'connected',
|
|
114
121
|
}
|
|
115
122
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {type Message, type Status} from '@sanity/comlink'
|
|
2
1
|
import {renderHook, waitFor} from '@testing-library/react'
|
|
3
2
|
import {describe, expect, it, vi} from 'vitest'
|
|
4
3
|
|
|
5
|
-
import {useWindowConnection
|
|
4
|
+
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
6
5
|
import {useStudioWorkspacesByProjectIdDataset} from './useStudioWorkspacesByProjectIdDataset'
|
|
7
6
|
|
|
8
7
|
vi.mock('../comlink/useWindowConnection', () => ({
|
|
@@ -54,49 +53,15 @@ describe('useStudioWorkspacesByResourceId', () => {
|
|
|
54
53
|
vi.clearAllMocks()
|
|
55
54
|
})
|
|
56
55
|
|
|
57
|
-
it('should return empty workspaces and connected=false when not connected', async () => {
|
|
58
|
-
// Create a mock that captures the onStatus callback
|
|
59
|
-
let capturedOnStatus: ((status: Status) => void) | undefined
|
|
60
|
-
|
|
61
|
-
vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
|
|
62
|
-
capturedOnStatus = onStatus
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
fetch: undefined,
|
|
66
|
-
sendMessage: vi.fn(),
|
|
67
|
-
} as unknown as WindowConnection<Message>
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const {result} = renderHook(() => useStudioWorkspacesByProjectIdDataset())
|
|
71
|
-
|
|
72
|
-
// Call onStatus with 'idle' to simulate not connected
|
|
73
|
-
if (capturedOnStatus) capturedOnStatus('idle')
|
|
74
|
-
|
|
75
|
-
expect(result.current).toEqual({
|
|
76
|
-
workspacesByProjectIdAndDataset: {},
|
|
77
|
-
error: null,
|
|
78
|
-
isConnected: false,
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
56
|
it('should process workspaces into lookup by projectId:dataset', async () => {
|
|
83
57
|
const mockFetch = vi.fn().mockResolvedValue(mockWorkspaceData)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
capturedOnStatus = onStatus
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
fetch: mockFetch,
|
|
91
|
-
sendMessage: vi.fn(),
|
|
92
|
-
} as unknown as WindowConnection<Message>
|
|
58
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
59
|
+
fetch: mockFetch,
|
|
60
|
+
sendMessage: vi.fn(),
|
|
93
61
|
})
|
|
94
62
|
|
|
95
63
|
const {result} = renderHook(() => useStudioWorkspacesByProjectIdDataset())
|
|
96
64
|
|
|
97
|
-
// Call onStatus with 'connected' to simulate connected state
|
|
98
|
-
if (capturedOnStatus) capturedOnStatus('connected')
|
|
99
|
-
|
|
100
65
|
await waitFor(() => {
|
|
101
66
|
expect(result.current.workspacesByProjectIdAndDataset).toEqual({
|
|
102
67
|
'project1:dataset1': [
|
|
@@ -138,38 +103,23 @@ describe('useStudioWorkspacesByResourceId', () => {
|
|
|
138
103
|
],
|
|
139
104
|
})
|
|
140
105
|
expect(result.current.error).toBeNull()
|
|
141
|
-
expect(result.current.isConnected).toBe(true)
|
|
142
106
|
})
|
|
143
107
|
|
|
144
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
145
|
-
'dashboard/v1/bridge/context',
|
|
146
|
-
undefined,
|
|
147
|
-
expect.any(Object),
|
|
148
|
-
)
|
|
108
|
+
expect(mockFetch).toHaveBeenCalledWith('dashboard/v1/context', undefined, expect.any(Object))
|
|
149
109
|
})
|
|
150
110
|
|
|
151
111
|
it('should handle fetch errors', async () => {
|
|
152
112
|
const mockFetch = vi.fn().mockRejectedValue(new Error('Failed to fetch'))
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
capturedOnStatus = onStatus
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
fetch: mockFetch,
|
|
160
|
-
sendMessage: vi.fn(),
|
|
161
|
-
} as unknown as WindowConnection<Message>
|
|
113
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
114
|
+
fetch: mockFetch,
|
|
115
|
+
sendMessage: vi.fn(),
|
|
162
116
|
})
|
|
163
117
|
|
|
164
118
|
const {result} = renderHook(() => useStudioWorkspacesByProjectIdDataset())
|
|
165
119
|
|
|
166
|
-
// Call onStatus with 'connected' to simulate connected state
|
|
167
|
-
if (capturedOnStatus) capturedOnStatus('connected')
|
|
168
|
-
|
|
169
120
|
await waitFor(() => {
|
|
170
121
|
expect(result.current.workspacesByProjectIdAndDataset).toEqual({})
|
|
171
122
|
expect(result.current.error).toBe('Failed to fetch workspaces')
|
|
172
|
-
expect(result.current.isConnected).toBe(true)
|
|
173
123
|
})
|
|
174
124
|
})
|
|
175
125
|
|
|
@@ -177,26 +127,16 @@ describe('useStudioWorkspacesByResourceId', () => {
|
|
|
177
127
|
const abortError = new Error('Aborted')
|
|
178
128
|
abortError.name = 'AbortError'
|
|
179
129
|
const mockFetch = vi.fn().mockRejectedValue(abortError)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
capturedOnStatus = onStatus
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
fetch: mockFetch,
|
|
187
|
-
sendMessage: vi.fn(),
|
|
188
|
-
} as unknown as WindowConnection<Message>
|
|
130
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
131
|
+
fetch: mockFetch,
|
|
132
|
+
sendMessage: vi.fn(),
|
|
189
133
|
})
|
|
190
134
|
|
|
191
135
|
const {result} = renderHook(() => useStudioWorkspacesByProjectIdDataset())
|
|
192
136
|
|
|
193
|
-
// Call onStatus with 'connected' to simulate connected state
|
|
194
|
-
if (capturedOnStatus) capturedOnStatus('connected')
|
|
195
|
-
|
|
196
137
|
await waitFor(() => {
|
|
197
138
|
expect(result.current.workspacesByProjectIdAndDataset).toEqual({})
|
|
198
139
|
expect(result.current.error).toBeNull()
|
|
199
|
-
expect(result.current.isConnected).toBe(true)
|
|
200
140
|
})
|
|
201
141
|
})
|
|
202
142
|
|
|
@@ -248,24 +188,14 @@ describe('useStudioWorkspacesByResourceId', () => {
|
|
|
248
188
|
],
|
|
249
189
|
},
|
|
250
190
|
}
|
|
251
|
-
|
|
252
191
|
const mockFetch = vi.fn().mockResolvedValue(mockDataWithMixedResources)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
capturedOnStatus = onStatus
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
fetch: mockFetch,
|
|
260
|
-
sendMessage: vi.fn(),
|
|
261
|
-
} as unknown as WindowConnection<Message>
|
|
192
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
193
|
+
fetch: mockFetch,
|
|
194
|
+
sendMessage: vi.fn(),
|
|
262
195
|
})
|
|
263
196
|
|
|
264
197
|
const {result} = renderHook(() => useStudioWorkspacesByProjectIdDataset())
|
|
265
198
|
|
|
266
|
-
// Call onStatus with 'connected' to simulate connected state
|
|
267
|
-
if (capturedOnStatus) capturedOnStatus('connected')
|
|
268
|
-
|
|
269
199
|
await waitFor(() => {
|
|
270
200
|
// Should only include the studio resource with valid projectId and dataset
|
|
271
201
|
expect(result.current.workspacesByProjectIdAndDataset['project1:dataset1']).toHaveLength(1)
|
|
@@ -285,7 +215,6 @@ describe('useStudioWorkspacesByResourceId', () => {
|
|
|
285
215
|
).toEqual(['studio-no-project', 'studio-no-dataset'])
|
|
286
216
|
|
|
287
217
|
expect(result.current.error).toBeNull()
|
|
288
|
-
expect(result.current.isConnected).toBe(true)
|
|
289
218
|
})
|
|
290
219
|
})
|
|
291
220
|
})
|