@sanity/sdk-react 2.3.1 → 2.5.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.
- package/README.md +575 -4
- package/dist/index.d.ts +260 -4
- package/dist/index.js +227 -47
- package/dist/index.js.map +1 -1
- package/package.json +22 -20
- package/src/_exports/sdk-react.ts +12 -0
- package/src/components/auth/LoginError.tsx +27 -7
- package/src/context/ComlinkTokenRefresh.test.tsx +107 -23
- package/src/hooks/agent/agentActions.test.tsx +78 -0
- package/src/hooks/agent/agentActions.ts +136 -0
- package/src/hooks/agent/useAgentResourceContext.test.tsx +245 -0
- package/src/hooks/agent/useAgentResourceContext.ts +106 -0
- package/src/hooks/client/useClient.test.tsx +42 -0
- package/src/hooks/client/useClient.ts +11 -4
- package/src/hooks/dashboard/types.ts +12 -0
- package/src/hooks/dashboard/useDispatchIntent.test.ts +239 -0
- package/src/hooks/dashboard/useDispatchIntent.ts +158 -0
- package/src/hooks/dashboard/utils/getResourceIdFromDocumentHandle.test.ts +155 -0
- package/src/hooks/dashboard/utils/getResourceIdFromDocumentHandle.ts +53 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +124 -9
- package/src/hooks/document/useApplyDocumentActions.ts +75 -4
- package/src/hooks/document/useDocumentPermissions.test.tsx +3 -3
- package/src/hooks/document/useDocumentPermissions.ts +9 -6
- package/src/hooks/projection/useDocumentProjection.ts +14 -3
- package/src/hooks/releases/usePerspective.test.tsx +1 -0
- package/src/hooks/releases/usePerspective.ts +1 -1
- package/src/hooks/users/useUsers.ts +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {renderHook} from '@testing-library/react'
|
|
2
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {AppProviders} from '../../../test/test-utils'
|
|
5
|
+
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
6
|
+
import {useAgentResourceContext} from './useAgentResourceContext'
|
|
7
|
+
|
|
8
|
+
vi.mock('../comlink/useWindowConnection', () => ({
|
|
9
|
+
useWindowConnection: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
describe('useAgentResourceContext', () => {
|
|
13
|
+
let mockSendMessage = vi.fn()
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockSendMessage = vi.fn()
|
|
17
|
+
vi.mocked(useWindowConnection).mockImplementation(() => {
|
|
18
|
+
return {
|
|
19
|
+
sendMessage: mockSendMessage,
|
|
20
|
+
fetch: vi.fn(),
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should send context update on mount', () => {
|
|
26
|
+
renderHook(
|
|
27
|
+
() =>
|
|
28
|
+
useAgentResourceContext({
|
|
29
|
+
projectId: 'test-project',
|
|
30
|
+
dataset: 'production',
|
|
31
|
+
documentId: 'doc-123',
|
|
32
|
+
}),
|
|
33
|
+
{wrapper: AppProviders},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/agent/resource/update', {
|
|
37
|
+
projectId: 'test-project',
|
|
38
|
+
dataset: 'production',
|
|
39
|
+
documentId: 'doc-123',
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('should send context update without documentId', () => {
|
|
44
|
+
renderHook(
|
|
45
|
+
() =>
|
|
46
|
+
useAgentResourceContext({
|
|
47
|
+
projectId: 'test-project',
|
|
48
|
+
dataset: 'production',
|
|
49
|
+
}),
|
|
50
|
+
{wrapper: AppProviders},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/agent/resource/update', {
|
|
54
|
+
projectId: 'test-project',
|
|
55
|
+
dataset: 'production',
|
|
56
|
+
documentId: undefined,
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should send context update when context changes', () => {
|
|
61
|
+
const {rerender} = renderHook(
|
|
62
|
+
({documentId}: {documentId: string}) =>
|
|
63
|
+
useAgentResourceContext({
|
|
64
|
+
projectId: 'test-project',
|
|
65
|
+
dataset: 'production',
|
|
66
|
+
documentId,
|
|
67
|
+
}),
|
|
68
|
+
{
|
|
69
|
+
wrapper: AppProviders,
|
|
70
|
+
initialProps: {documentId: 'doc-123'},
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(1)
|
|
75
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/agent/resource/update', {
|
|
76
|
+
projectId: 'test-project',
|
|
77
|
+
dataset: 'production',
|
|
78
|
+
documentId: 'doc-123',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Change documentId
|
|
82
|
+
rerender({documentId: 'doc-456'})
|
|
83
|
+
|
|
84
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(2)
|
|
85
|
+
expect(mockSendMessage).toHaveBeenLastCalledWith('dashboard/v1/events/agent/resource/update', {
|
|
86
|
+
projectId: 'test-project',
|
|
87
|
+
dataset: 'production',
|
|
88
|
+
documentId: 'doc-456',
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should not send duplicate updates for the same context', () => {
|
|
93
|
+
const {rerender} = renderHook(
|
|
94
|
+
({documentId}: {documentId: string}) =>
|
|
95
|
+
useAgentResourceContext({
|
|
96
|
+
projectId: 'test-project',
|
|
97
|
+
dataset: 'production',
|
|
98
|
+
documentId,
|
|
99
|
+
}),
|
|
100
|
+
{
|
|
101
|
+
wrapper: AppProviders,
|
|
102
|
+
initialProps: {documentId: 'doc-123'},
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(1)
|
|
107
|
+
|
|
108
|
+
// Re-render with the same documentId
|
|
109
|
+
rerender({documentId: 'doc-123'})
|
|
110
|
+
|
|
111
|
+
// Should still only be called once
|
|
112
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(1)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should warn when projectId is missing', () => {
|
|
116
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
117
|
+
|
|
118
|
+
renderHook(
|
|
119
|
+
() =>
|
|
120
|
+
useAgentResourceContext({
|
|
121
|
+
projectId: '',
|
|
122
|
+
dataset: 'production',
|
|
123
|
+
documentId: 'doc-123',
|
|
124
|
+
}),
|
|
125
|
+
{wrapper: AppProviders},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
129
|
+
'[useAgentResourceContext] projectId and dataset are required',
|
|
130
|
+
{projectId: '', dataset: 'production'},
|
|
131
|
+
)
|
|
132
|
+
expect(mockSendMessage).not.toHaveBeenCalled()
|
|
133
|
+
|
|
134
|
+
consoleWarnSpy.mockRestore()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should warn when dataset is missing', () => {
|
|
138
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
139
|
+
|
|
140
|
+
renderHook(
|
|
141
|
+
() =>
|
|
142
|
+
useAgentResourceContext({
|
|
143
|
+
projectId: 'test-project',
|
|
144
|
+
dataset: '',
|
|
145
|
+
documentId: 'doc-123',
|
|
146
|
+
}),
|
|
147
|
+
{wrapper: AppProviders},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
151
|
+
'[useAgentResourceContext] projectId and dataset are required',
|
|
152
|
+
{projectId: 'test-project', dataset: ''},
|
|
153
|
+
)
|
|
154
|
+
expect(mockSendMessage).not.toHaveBeenCalled()
|
|
155
|
+
|
|
156
|
+
consoleWarnSpy.mockRestore()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should handle errors when sending messages', () => {
|
|
160
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
161
|
+
mockSendMessage.mockImplementation(() => {
|
|
162
|
+
throw new Error('Failed to send message')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Should not throw, but should log error
|
|
166
|
+
expect(() =>
|
|
167
|
+
renderHook(
|
|
168
|
+
() =>
|
|
169
|
+
useAgentResourceContext({
|
|
170
|
+
projectId: 'test-project',
|
|
171
|
+
dataset: 'production',
|
|
172
|
+
documentId: 'doc-123',
|
|
173
|
+
}),
|
|
174
|
+
{wrapper: AppProviders},
|
|
175
|
+
),
|
|
176
|
+
).not.toThrow()
|
|
177
|
+
|
|
178
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
179
|
+
'[useAgentResourceContext] Failed to update context:',
|
|
180
|
+
expect.any(Error),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
consoleErrorSpy.mockRestore()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should update context when switching between documents', () => {
|
|
187
|
+
const {rerender} = renderHook(
|
|
188
|
+
({documentId}: {documentId: string}) =>
|
|
189
|
+
useAgentResourceContext({
|
|
190
|
+
projectId: 'test-project',
|
|
191
|
+
dataset: 'production',
|
|
192
|
+
documentId,
|
|
193
|
+
}),
|
|
194
|
+
{
|
|
195
|
+
wrapper: AppProviders,
|
|
196
|
+
initialProps: {documentId: 'doc-123'},
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(1)
|
|
201
|
+
|
|
202
|
+
// Switch to document 456
|
|
203
|
+
rerender({documentId: 'doc-456'})
|
|
204
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(2)
|
|
205
|
+
|
|
206
|
+
// Switch to document 789
|
|
207
|
+
rerender({documentId: 'doc-789'})
|
|
208
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(3)
|
|
209
|
+
|
|
210
|
+
// Switch back to document 123
|
|
211
|
+
rerender({documentId: 'doc-123'})
|
|
212
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(4)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should update context when document is cleared', () => {
|
|
216
|
+
const {rerender} = renderHook(
|
|
217
|
+
({documentId}: {documentId: string | undefined}) =>
|
|
218
|
+
useAgentResourceContext({
|
|
219
|
+
projectId: 'test-project',
|
|
220
|
+
dataset: 'production',
|
|
221
|
+
documentId,
|
|
222
|
+
}),
|
|
223
|
+
{
|
|
224
|
+
wrapper: AppProviders,
|
|
225
|
+
initialProps: {documentId: 'doc-123' as string | undefined},
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/agent/resource/update', {
|
|
230
|
+
projectId: 'test-project',
|
|
231
|
+
dataset: 'production',
|
|
232
|
+
documentId: 'doc-123',
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Clear documentId
|
|
236
|
+
rerender({documentId: undefined})
|
|
237
|
+
|
|
238
|
+
expect(mockSendMessage).toHaveBeenCalledTimes(2)
|
|
239
|
+
expect(mockSendMessage).toHaveBeenLastCalledWith('dashboard/v1/events/agent/resource/update', {
|
|
240
|
+
projectId: 'test-project',
|
|
241
|
+
dataset: 'production',
|
|
242
|
+
documentId: undefined,
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
2
|
+
import {type FrameMessage} from '@sanity/sdk'
|
|
3
|
+
import {useCallback, useEffect, useRef} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export interface AgentResourceContextOptions {
|
|
11
|
+
/**
|
|
12
|
+
* The project ID of the current context
|
|
13
|
+
*/
|
|
14
|
+
projectId: string
|
|
15
|
+
/**
|
|
16
|
+
* The dataset of the current context
|
|
17
|
+
*/
|
|
18
|
+
dataset: string
|
|
19
|
+
/**
|
|
20
|
+
* Optional document ID if the user is viewing/editing a specific document
|
|
21
|
+
*/
|
|
22
|
+
documentId?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @public
|
|
27
|
+
* Hook for emitting agent resource context updates to the Dashboard.
|
|
28
|
+
* This allows the Agent to understand what resource the user is currently
|
|
29
|
+
* interacting with (e.g., which document they're editing).
|
|
30
|
+
*
|
|
31
|
+
* The hook will automatically emit the context when it changes, and also
|
|
32
|
+
* emit the initial context when the hook is first mounted.
|
|
33
|
+
*
|
|
34
|
+
* @category Agent
|
|
35
|
+
* @param options - The resource context options containing projectId, dataset, and optional documentId
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* import {useAgentResourceContext} from '@sanity/sdk-react'
|
|
40
|
+
*
|
|
41
|
+
* function MyComponent() {
|
|
42
|
+
* const documentId = 'my-document-id'
|
|
43
|
+
*
|
|
44
|
+
* // Automatically updates the Agent's context whenever the document changes
|
|
45
|
+
* useAgentResourceContext({
|
|
46
|
+
* projectId: 'my-project',
|
|
47
|
+
* dataset: 'production',
|
|
48
|
+
* documentId,
|
|
49
|
+
* })
|
|
50
|
+
*
|
|
51
|
+
* return <div>Editing document: {documentId}</div>
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function useAgentResourceContext(options: AgentResourceContextOptions): void {
|
|
56
|
+
const {projectId, dataset, documentId} = options
|
|
57
|
+
const {sendMessage} = useWindowConnection<Events.AgentResourceUpdateMessage, FrameMessage>({
|
|
58
|
+
name: SDK_NODE_NAME,
|
|
59
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Track the last sent context to avoid duplicate updates
|
|
63
|
+
const lastContextRef = useRef<string | null>(null)
|
|
64
|
+
|
|
65
|
+
const updateContext = useCallback(() => {
|
|
66
|
+
// Validate required fields
|
|
67
|
+
if (!projectId || !dataset) {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.warn('[useAgentResourceContext] projectId and dataset are required', {
|
|
70
|
+
projectId,
|
|
71
|
+
dataset,
|
|
72
|
+
})
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create a stable key for the current context
|
|
77
|
+
const contextKey = `${projectId}:${dataset}:${documentId || ''}`
|
|
78
|
+
|
|
79
|
+
// Skip if context hasn't changed
|
|
80
|
+
if (lastContextRef.current === contextKey) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const message: Events.AgentResourceUpdateMessage = {
|
|
86
|
+
type: 'dashboard/v1/events/agent/resource/update',
|
|
87
|
+
data: {
|
|
88
|
+
projectId,
|
|
89
|
+
dataset,
|
|
90
|
+
documentId,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
sendMessage(message.type, message.data)
|
|
95
|
+
lastContextRef.current = contextKey
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.error('[useAgentResourceContext] Failed to update context:', error)
|
|
99
|
+
}
|
|
100
|
+
}, [projectId, dataset, documentId, sendMessage])
|
|
101
|
+
|
|
102
|
+
// Update context whenever it changes
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
updateContext()
|
|
105
|
+
}, [updateContext])
|
|
106
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {renderHook} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
5
|
+
import {useClient} from './useClient'
|
|
6
|
+
|
|
7
|
+
describe('useClient', () => {
|
|
8
|
+
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
9
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
10
|
+
{children}
|
|
11
|
+
</ResourceProvider>
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
it('should throw a helpful error when called without options', () => {
|
|
15
|
+
// Suppress console.error for this test since we expect an error to be thrown
|
|
16
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
17
|
+
|
|
18
|
+
expect(() => {
|
|
19
|
+
// @ts-expect-error Testing missing options
|
|
20
|
+
renderHook(() => useClient(), {wrapper})
|
|
21
|
+
}).toThrowError(/requires a configuration object with at least an "apiVersion" property/)
|
|
22
|
+
|
|
23
|
+
consoleErrorSpy.mockRestore()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should throw a helpful error when called with undefined', () => {
|
|
27
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
28
|
+
|
|
29
|
+
expect(() => {
|
|
30
|
+
// @ts-expect-error Testing undefined options
|
|
31
|
+
renderHook(() => useClient(undefined), {wrapper})
|
|
32
|
+
}).toThrowError(/requires a configuration object with at least an "apiVersion" property/)
|
|
33
|
+
|
|
34
|
+
consoleErrorSpy.mockRestore()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should return a client when called with valid options', () => {
|
|
38
|
+
const {result} = renderHook(() => useClient({apiVersion: '2024-11-12'}), {wrapper})
|
|
39
|
+
expect(result.current).toBeDefined()
|
|
40
|
+
expect(result.current.fetch).toBeDefined()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {getClientState} from '@sanity/sdk'
|
|
2
|
-
import {identity} from 'rxjs'
|
|
1
|
+
import {type ClientOptions, getClientState, type SanityInstance} from '@sanity/sdk'
|
|
3
2
|
|
|
4
3
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
4
|
|
|
@@ -31,6 +30,14 @@ import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
|
31
30
|
* @function
|
|
32
31
|
*/
|
|
33
32
|
export const useClient = createStateSourceHook({
|
|
34
|
-
getState:
|
|
35
|
-
|
|
33
|
+
getState: (instance: SanityInstance, options: ClientOptions) => {
|
|
34
|
+
if (!options || typeof options !== 'object') {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'useClient() requires a configuration object with at least an "apiVersion" property. ' +
|
|
37
|
+
'Example: useClient({ apiVersion: "2024-11-12" })',
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
return getClientState(instance, options)
|
|
41
|
+
},
|
|
42
|
+
getConfig: (options: ClientOptions) => options,
|
|
36
43
|
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {type DocumentHandle, type DocumentSource} from '@sanity/sdk'
|
|
2
|
+
/**
|
|
3
|
+
* Document handle that optionally includes a source (e.g., media library source)
|
|
4
|
+
* or projectId and dataset for traditional dataset sources
|
|
5
|
+
* (but now marked optional since it's valid to just use a source)
|
|
6
|
+
* @beta
|
|
7
|
+
*/
|
|
8
|
+
export interface DocumentHandleWithSource extends Omit<DocumentHandle, 'projectId' | 'dataset'> {
|
|
9
|
+
source?: DocumentSource
|
|
10
|
+
projectId?: string
|
|
11
|
+
dataset?: string
|
|
12
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import {canvasSource, type DocumentHandle, mediaLibrarySource} from '@sanity/sdk'
|
|
2
|
+
import {renderHook} from '@testing-library/react'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {type DocumentHandleWithSource} from './types'
|
|
6
|
+
import {useDispatchIntent} from './useDispatchIntent'
|
|
7
|
+
|
|
8
|
+
// Mock the useWindowConnection hook
|
|
9
|
+
const mockSendMessage = vi.fn()
|
|
10
|
+
vi.mock('../comlink/useWindowConnection', () => ({
|
|
11
|
+
useWindowConnection: vi.fn(() => ({
|
|
12
|
+
sendMessage: mockSendMessage,
|
|
13
|
+
})),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('useDispatchIntent', () => {
|
|
17
|
+
const mockDocumentHandle: DocumentHandle = {
|
|
18
|
+
documentId: 'test-document-id',
|
|
19
|
+
documentType: 'test-document-type',
|
|
20
|
+
projectId: 'test-project-id',
|
|
21
|
+
dataset: 'test-dataset',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks()
|
|
26
|
+
// Reset mock implementation to default behavior
|
|
27
|
+
mockSendMessage.mockImplementation(() => {})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return dispatchIntent function', () => {
|
|
31
|
+
const {result} = renderHook(() =>
|
|
32
|
+
useDispatchIntent({action: 'edit', documentHandle: mockDocumentHandle}),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(result.current).toEqual({
|
|
36
|
+
dispatchIntent: expect.any(Function),
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should throw error when neither action nor intentId is provided', () => {
|
|
41
|
+
const {result} = renderHook(() => useDispatchIntent({documentHandle: mockDocumentHandle}))
|
|
42
|
+
|
|
43
|
+
expect(() => result.current.dispatchIntent()).toThrow(
|
|
44
|
+
'useDispatchIntent: Either `action` or `intentId` must be provided.',
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should handle errors gracefully', () => {
|
|
49
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
50
|
+
mockSendMessage.mockImplementation(() => {
|
|
51
|
+
throw new Error('Test error')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const {result} = renderHook(() =>
|
|
55
|
+
useDispatchIntent({action: 'edit', documentHandle: mockDocumentHandle}),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
expect(() => result.current.dispatchIntent()).toThrow('Test error')
|
|
59
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to dispatch intent:', expect.any(Error))
|
|
60
|
+
|
|
61
|
+
consoleErrorSpy.mockRestore()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should use memoized dispatchIntent function', () => {
|
|
65
|
+
const {result, rerender} = renderHook(({params}) => useDispatchIntent(params), {
|
|
66
|
+
initialProps: {params: {action: 'edit' as const, documentHandle: mockDocumentHandle}},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const firstDispatchIntent = result.current.dispatchIntent
|
|
70
|
+
|
|
71
|
+
// Rerender with the same params
|
|
72
|
+
rerender({params: {action: 'edit' as const, documentHandle: mockDocumentHandle}})
|
|
73
|
+
|
|
74
|
+
expect(result.current.dispatchIntent).toBe(firstDispatchIntent)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should create new dispatchIntent function when documentHandle changes', () => {
|
|
78
|
+
const {result, rerender} = renderHook(({params}) => useDispatchIntent(params), {
|
|
79
|
+
initialProps: {params: {action: 'edit' as const, documentHandle: mockDocumentHandle}},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const firstDispatchIntent = result.current.dispatchIntent
|
|
83
|
+
|
|
84
|
+
const newDocumentHandle: DocumentHandle = {
|
|
85
|
+
documentId: 'new-document-id',
|
|
86
|
+
documentType: 'new-document-type',
|
|
87
|
+
projectId: 'new-project-id',
|
|
88
|
+
dataset: 'new-dataset',
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
rerender({params: {action: 'edit' as const, documentHandle: newDocumentHandle}})
|
|
92
|
+
|
|
93
|
+
expect(result.current.dispatchIntent).not.toBe(firstDispatchIntent)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should warn if both action and intentId are provided', () => {
|
|
97
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
98
|
+
const {result} = renderHook(() =>
|
|
99
|
+
useDispatchIntent({
|
|
100
|
+
action: 'edit' as const,
|
|
101
|
+
intentId: 'custom-intent' as never, // test runtime error when both are provided
|
|
102
|
+
documentHandle: mockDocumentHandle,
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
result.current.dispatchIntent()
|
|
106
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
107
|
+
'useDispatchIntent: Both `action` and `intentId` were provided. Using `intentId` and ignoring `action`.',
|
|
108
|
+
)
|
|
109
|
+
consoleWarnSpy.mockRestore()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should send intent message with action and params when both are provided', () => {
|
|
113
|
+
const intentParams = {view: 'editor', tab: 'content'}
|
|
114
|
+
const {result} = renderHook(() =>
|
|
115
|
+
useDispatchIntent({
|
|
116
|
+
action: 'edit',
|
|
117
|
+
documentHandle: mockDocumentHandle,
|
|
118
|
+
parameters: intentParams,
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
result.current.dispatchIntent()
|
|
123
|
+
|
|
124
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
125
|
+
action: 'edit',
|
|
126
|
+
document: {
|
|
127
|
+
id: 'test-document-id',
|
|
128
|
+
type: 'test-document-type',
|
|
129
|
+
},
|
|
130
|
+
resource: {
|
|
131
|
+
id: 'test-project-id.test-dataset',
|
|
132
|
+
},
|
|
133
|
+
parameters: intentParams,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should send intent message with intentId and params when both are provided', () => {
|
|
138
|
+
const intentParams = {view: 'editor', tab: 'content'}
|
|
139
|
+
const {result} = renderHook(() =>
|
|
140
|
+
useDispatchIntent({
|
|
141
|
+
intentId: 'custom-intent',
|
|
142
|
+
documentHandle: mockDocumentHandle,
|
|
143
|
+
parameters: intentParams,
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
result.current.dispatchIntent()
|
|
148
|
+
|
|
149
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
150
|
+
intentId: 'custom-intent',
|
|
151
|
+
document: {
|
|
152
|
+
id: 'test-document-id',
|
|
153
|
+
type: 'test-document-type',
|
|
154
|
+
},
|
|
155
|
+
resource: {
|
|
156
|
+
id: 'test-project-id.test-dataset',
|
|
157
|
+
},
|
|
158
|
+
parameters: intentParams,
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should send intent message with media library source', () => {
|
|
163
|
+
const mockMediaLibraryHandle: DocumentHandleWithSource = {
|
|
164
|
+
documentId: 'test-asset-id',
|
|
165
|
+
documentType: 'sanity.asset',
|
|
166
|
+
source: mediaLibrarySource('mlPGY7BEqt52'),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const {result} = renderHook(() =>
|
|
170
|
+
useDispatchIntent({
|
|
171
|
+
action: 'edit',
|
|
172
|
+
documentHandle: mockMediaLibraryHandle,
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
result.current.dispatchIntent()
|
|
177
|
+
|
|
178
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
179
|
+
action: 'edit',
|
|
180
|
+
document: {
|
|
181
|
+
id: 'test-asset-id',
|
|
182
|
+
type: 'sanity.asset',
|
|
183
|
+
},
|
|
184
|
+
resource: {
|
|
185
|
+
id: 'mlPGY7BEqt52',
|
|
186
|
+
type: 'media-library',
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should send intent message with canvas source', () => {
|
|
192
|
+
const mockCanvasHandle: DocumentHandleWithSource = {
|
|
193
|
+
documentId: 'test-canvas-document-id',
|
|
194
|
+
documentType: 'sanity.canvas.document',
|
|
195
|
+
source: canvasSource('canvas123'),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const {result} = renderHook(() =>
|
|
199
|
+
useDispatchIntent({
|
|
200
|
+
action: 'edit',
|
|
201
|
+
documentHandle: mockCanvasHandle,
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
result.current.dispatchIntent()
|
|
206
|
+
|
|
207
|
+
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
208
|
+
action: 'edit',
|
|
209
|
+
document: {
|
|
210
|
+
id: 'test-canvas-document-id',
|
|
211
|
+
type: 'sanity.canvas.document',
|
|
212
|
+
},
|
|
213
|
+
resource: {
|
|
214
|
+
id: 'canvas123',
|
|
215
|
+
type: 'canvas',
|
|
216
|
+
},
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('error handling', () => {
|
|
221
|
+
it('should throw error when neither source nor projectId/dataset is provided', () => {
|
|
222
|
+
const invalidHandle = {
|
|
223
|
+
documentId: 'test-document-id',
|
|
224
|
+
documentType: 'test-document-type',
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const {result} = renderHook(() =>
|
|
228
|
+
useDispatchIntent({
|
|
229
|
+
action: 'edit',
|
|
230
|
+
documentHandle: invalidHandle as unknown as DocumentHandleWithSource,
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect(() => result.current.dispatchIntent()).toThrow(
|
|
235
|
+
'useDispatchIntent: Either `source` or both `projectId` and `dataset` must be provided in documentHandle.',
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
})
|