@sanity/sdk-react 2.4.0 → 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 +50 -0
- package/dist/index.js +101 -14
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/src/_exports/sdk-react.ts +4 -0
- package/src/hooks/agent/useAgentResourceContext.test.tsx +245 -0
- package/src/hooks/agent/useAgentResourceContext.ts +106 -0
- package/src/hooks/dashboard/useDispatchIntent.test.ts +1 -4
- package/src/hooks/document/useApplyDocumentActions.test.ts +124 -9
- package/src/hooks/document/useApplyDocumentActions.ts +44 -4
- package/src/hooks/document/useDocumentPermissions.test.tsx +3 -3
- package/src/hooks/document/useDocumentPermissions.ts +9 -6
- package/src/hooks/releases/usePerspective.test.tsx +1 -0
- package/src/hooks/releases/usePerspective.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "git+
|
|
21
|
+
"url": "git+https://github.com/sanity-io/sdk.git",
|
|
22
|
+
"directory": "packages/react"
|
|
22
23
|
},
|
|
23
24
|
"license": "MIT",
|
|
24
25
|
"author": "Sanity <developers@sanity.io>",
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"prettier": "@sanity/prettier-config",
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@sanity/client": "^7.12.0",
|
|
46
|
-
"@sanity/message-protocol": "^0.
|
|
47
|
+
"@sanity/message-protocol": "^0.18.0",
|
|
47
48
|
"@sanity/types": "^3.83.0",
|
|
48
49
|
"@types/lodash-es": "^4.17.12",
|
|
49
50
|
"groq": "3.88.1-typegen-experimental.0",
|
|
@@ -51,12 +52,12 @@
|
|
|
51
52
|
"react-compiler-runtime": "19.1.0-rc.2",
|
|
52
53
|
"react-error-boundary": "^5.0.0",
|
|
53
54
|
"rxjs": "^7.8.2",
|
|
54
|
-
"@sanity/sdk": "2.
|
|
55
|
+
"@sanity/sdk": "2.5.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@sanity/browserslist-config": "^1.0.5",
|
|
58
59
|
"@sanity/comlink": "^3.1.1",
|
|
59
|
-
"@sanity/pkg-utils": "^
|
|
60
|
+
"@sanity/pkg-utils": "^8.1.29",
|
|
60
61
|
"@sanity/prettier-config": "^1.0.6",
|
|
61
62
|
"@testing-library/jest-dom": "^6.9.1",
|
|
62
63
|
"@testing-library/react": "^16.3.0",
|
|
@@ -77,17 +78,17 @@
|
|
|
77
78
|
"vite": "^6.3.4",
|
|
78
79
|
"vitest": "^3.2.4",
|
|
79
80
|
"@repo/config-eslint": "0.0.0",
|
|
80
|
-
"@repo/package.bundle": "3.82.0",
|
|
81
81
|
"@repo/config-test": "0.0.1",
|
|
82
82
|
"@repo/package.config": "0.0.1",
|
|
83
|
-
"@repo/tsconfig": "0.0.1"
|
|
83
|
+
"@repo/tsconfig": "0.0.1",
|
|
84
|
+
"@repo/package.bundle": "3.82.0"
|
|
84
85
|
},
|
|
85
86
|
"peerDependencies": {
|
|
86
87
|
"react": "^18.0.0 || ^19.0.0",
|
|
87
88
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
88
89
|
},
|
|
89
90
|
"engines": {
|
|
90
|
-
"node": ">=20.
|
|
91
|
+
"node": ">=20.19"
|
|
91
92
|
},
|
|
92
93
|
"publishConfig": {
|
|
93
94
|
"access": "public"
|
|
@@ -13,6 +13,10 @@ export {
|
|
|
13
13
|
useAgentTransform,
|
|
14
14
|
useAgentTranslate,
|
|
15
15
|
} from '../hooks/agent/agentActions'
|
|
16
|
+
export {
|
|
17
|
+
type AgentResourceContextOptions,
|
|
18
|
+
useAgentResourceContext,
|
|
19
|
+
} from '../hooks/agent/useAgentResourceContext'
|
|
16
20
|
export {useAuthState} from '../hooks/auth/useAuthState'
|
|
17
21
|
export {useAuthToken} from '../hooks/auth/useAuthToken'
|
|
18
22
|
export {useCurrentUser} from '../hooks/auth/useCurrentUser'
|
|
@@ -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
|
+
}
|
|
@@ -38,10 +38,7 @@ describe('useDispatchIntent', () => {
|
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
it('should throw error when neither action nor intentId is provided', () => {
|
|
41
|
-
const {result} = renderHook(() =>
|
|
42
|
-
// @ts-expect-error - Testing runtime error when neither is provided
|
|
43
|
-
useDispatchIntent({documentHandle: mockDocumentHandle}),
|
|
44
|
-
)
|
|
41
|
+
const {result} = renderHook(() => useDispatchIntent({documentHandle: mockDocumentHandle}))
|
|
45
42
|
|
|
46
43
|
expect(() => result.current.dispatchIntent()).toThrow(
|
|
47
44
|
'useDispatchIntent: Either `action` or `intentId` must be provided.',
|
|
@@ -1,20 +1,135 @@
|
|
|
1
|
-
import {applyDocumentActions} from '@sanity/sdk'
|
|
1
|
+
import {applyDocumentActions, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {describe, it} from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
5
6
|
|
|
6
|
-
vi.mock('../helpers/createCallbackHook', () => ({
|
|
7
|
-
createCallbackHook: vi.fn((cb) => () => cb),
|
|
8
|
-
}))
|
|
9
7
|
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
10
8
|
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
11
9
|
return {...original, applyDocumentActions: vi.fn()}
|
|
12
10
|
})
|
|
13
11
|
|
|
12
|
+
vi.mock('../context/useSanityInstance')
|
|
13
|
+
|
|
14
|
+
// These are quite fragile mocks, but they are useful enough for now.
|
|
15
|
+
const instances: Record<string, SanityInstance | undefined> = {
|
|
16
|
+
'p123.d': {__id: 'p123.d'} as unknown as SanityInstance,
|
|
17
|
+
'p.d123': {__id: 'p.d123'} as unknown as SanityInstance,
|
|
18
|
+
'p123.d123': {__id: 'p123.d123'} as unknown as SanityInstance,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const instance = {
|
|
22
|
+
match({projectId = 'p', dataset = 'd'}): SanityInstance | undefined {
|
|
23
|
+
return instances[`${projectId}.${dataset}`]
|
|
24
|
+
},
|
|
25
|
+
} as unknown as SanityInstance
|
|
26
|
+
|
|
14
27
|
describe('useApplyDocumentActions', () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.resetAllMocks()
|
|
30
|
+
vi.mocked(useSanityInstance).mockReturnValueOnce(instance)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('uses the SanityInstance', async () => {
|
|
34
|
+
const apply = useApplyDocumentActions()
|
|
35
|
+
apply({
|
|
36
|
+
type: 'document.edit',
|
|
37
|
+
documentType: 'post',
|
|
38
|
+
documentId: 'abc',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
42
|
+
actions: [
|
|
43
|
+
{
|
|
44
|
+
type: 'document.edit',
|
|
45
|
+
documentType: 'post',
|
|
46
|
+
documentId: 'abc',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('uses SanityInstance.match when projectId is overrideen', async () => {
|
|
53
|
+
const apply = useApplyDocumentActions()
|
|
54
|
+
apply({
|
|
55
|
+
type: 'document.edit',
|
|
56
|
+
documentType: 'post',
|
|
57
|
+
documentId: 'abc',
|
|
58
|
+
|
|
59
|
+
projectId: 'p123',
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instances['p123.d'], {
|
|
63
|
+
actions: [
|
|
64
|
+
{
|
|
65
|
+
type: 'document.edit',
|
|
66
|
+
documentType: 'post',
|
|
67
|
+
documentId: 'abc',
|
|
68
|
+
|
|
69
|
+
projectId: 'p123',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('uses SanityInstance when dataset is overrideen', async () => {
|
|
76
|
+
const apply = useApplyDocumentActions()
|
|
77
|
+
apply({
|
|
78
|
+
type: 'document.edit',
|
|
79
|
+
documentType: 'post',
|
|
80
|
+
documentId: 'abc',
|
|
81
|
+
|
|
82
|
+
dataset: 'd123',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
86
|
+
actions: [
|
|
87
|
+
{
|
|
88
|
+
type: 'document.edit',
|
|
89
|
+
documentType: 'post',
|
|
90
|
+
documentId: 'abc',
|
|
91
|
+
|
|
92
|
+
dataset: 'd123',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('uses SanityInstance.amcth when projectId and dataset is overrideen', async () => {
|
|
99
|
+
const apply = useApplyDocumentActions()
|
|
100
|
+
apply({
|
|
101
|
+
type: 'document.edit',
|
|
102
|
+
documentType: 'post',
|
|
103
|
+
documentId: 'abc',
|
|
104
|
+
|
|
105
|
+
projectId: 'p123',
|
|
106
|
+
dataset: 'd123',
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instances['p123.d123'], {
|
|
110
|
+
actions: [
|
|
111
|
+
{
|
|
112
|
+
type: 'document.edit',
|
|
113
|
+
documentType: 'post',
|
|
114
|
+
documentId: 'abc',
|
|
115
|
+
|
|
116
|
+
projectId: 'p123',
|
|
117
|
+
dataset: 'd123',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("throws if SanityInstance.match doesn't find anything", async () => {
|
|
124
|
+
const apply = useApplyDocumentActions()
|
|
125
|
+
expect(() => {
|
|
126
|
+
apply({
|
|
127
|
+
type: 'document.edit',
|
|
128
|
+
documentType: 'post',
|
|
129
|
+
documentId: 'abc',
|
|
130
|
+
|
|
131
|
+
projectId: 'other',
|
|
132
|
+
})
|
|
133
|
+
}).toThrow()
|
|
19
134
|
})
|
|
20
135
|
})
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from '@sanity/sdk'
|
|
7
7
|
import {type SanityDocument} from 'groq'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
10
10
|
// this import is used in an `{@link useEditDocument}`
|
|
11
11
|
// eslint-disable-next-line unused-imports/no-unused-imports, import/consistent-type-specifier-style
|
|
12
12
|
import type {useEditDocument} from './useEditDocument'
|
|
@@ -150,6 +150,46 @@ interface UseApplyDocumentActions {
|
|
|
150
150
|
* }
|
|
151
151
|
* ```
|
|
152
152
|
*/
|
|
153
|
-
export const useApplyDocumentActions =
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
export const useApplyDocumentActions: UseApplyDocumentActions = () => {
|
|
154
|
+
const instance = useSanityInstance()
|
|
155
|
+
|
|
156
|
+
return (actionOrActions, options) => {
|
|
157
|
+
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
|
|
158
|
+
|
|
159
|
+
let projectId
|
|
160
|
+
let dataset
|
|
161
|
+
for (const action of actions) {
|
|
162
|
+
if (action.projectId) {
|
|
163
|
+
if (!projectId) projectId = action.projectId
|
|
164
|
+
if (action.projectId !== projectId) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Mismatched project IDs found in actions. All actions must belong to the same project. Found "${action.projectId}" but expected "${projectId}".`,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (action.dataset) {
|
|
171
|
+
if (!dataset) dataset = action.dataset
|
|
172
|
+
if (action.dataset !== dataset) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`Mismatched datasets found in actions. All actions must belong to the same dataset. Found "${action.dataset}" but expected "${dataset}".`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (projectId || dataset) {
|
|
182
|
+
const actualInstance = instance.match({projectId, dataset})
|
|
183
|
+
if (!actualInstance) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Could not find a matching Sanity instance for the requested action: ${JSON.stringify({projectId, dataset}, null, 2)}.
|
|
186
|
+
Please ensure there is a ResourceProvider component with a matching configuration in the component hierarchy.`,
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return applyDocumentActions(actualInstance, {actions, ...options})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return applyDocumentActions(instance, {actions, ...options})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -97,7 +97,7 @@ describe('usePermissions', () => {
|
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
// ResourceProvider handles the instance configuration
|
|
100
|
-
expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), mockAction)
|
|
100
|
+
expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), {actions: [mockAction]})
|
|
101
101
|
expect(result.current).toEqual(mockPermissionAllowed)
|
|
102
102
|
})
|
|
103
103
|
|
|
@@ -140,7 +140,7 @@ describe('usePermissions', () => {
|
|
|
140
140
|
),
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
-
expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), actions)
|
|
143
|
+
expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), {actions})
|
|
144
144
|
})
|
|
145
145
|
|
|
146
146
|
it('should throw an error if actions have mismatched project IDs', () => {
|
|
@@ -226,7 +226,7 @@ describe('usePermissions', () => {
|
|
|
226
226
|
|
|
227
227
|
// Now it should render properly
|
|
228
228
|
await waitFor(() => {
|
|
229
|
-
expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), mockAction)
|
|
229
|
+
expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), {actions: [mockAction]})
|
|
230
230
|
})
|
|
231
231
|
})
|
|
232
232
|
|