@sanity/sdk-react 2.3.1 → 2.4.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/dist/index.d.ts +210 -4
- package/dist/index.js +129 -36
- package/dist/index.js.map +1 -1
- package/package.json +15 -14
- package/src/_exports/sdk-react.ts +8 -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/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 +242 -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.ts +31 -0
- package/src/hooks/projection/useDocumentProjection.ts +14 -3
- package/src/hooks/users/useUsers.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.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -51,30 +51,31 @@
|
|
|
51
51
|
"react-compiler-runtime": "19.1.0-rc.2",
|
|
52
52
|
"react-error-boundary": "^5.0.0",
|
|
53
53
|
"rxjs": "^7.8.2",
|
|
54
|
-
"@sanity/sdk": "2.
|
|
54
|
+
"@sanity/sdk": "2.4.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@sanity/browserslist-config": "^1.0.5",
|
|
58
|
-
"@sanity/comlink": "^3.
|
|
58
|
+
"@sanity/comlink": "^3.1.1",
|
|
59
59
|
"@sanity/pkg-utils": "^7.2.2",
|
|
60
|
-
"@sanity/prettier-config": "^1.0.
|
|
61
|
-
"@testing-library/jest-dom": "^6.
|
|
60
|
+
"@sanity/prettier-config": "^1.0.6",
|
|
61
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
62
62
|
"@testing-library/react": "^16.3.0",
|
|
63
|
-
"@types/
|
|
64
|
-
"@types/react
|
|
65
|
-
"@
|
|
66
|
-
"@
|
|
63
|
+
"@types/node": "^22.19.1",
|
|
64
|
+
"@types/react": "^19.2.7",
|
|
65
|
+
"@types/react-dom": "^19.2.3",
|
|
66
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
67
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
67
68
|
"babel-plugin-react-compiler": "19.1.0-rc.1",
|
|
68
69
|
"eslint": "^9.22.0",
|
|
69
|
-
"groq-js": "^1.
|
|
70
|
+
"groq-js": "^1.22.0",
|
|
70
71
|
"jsdom": "^25.0.1",
|
|
71
|
-
"prettier": "^3.
|
|
72
|
-
"react": "^19.1
|
|
73
|
-
"react-dom": "^19.1
|
|
72
|
+
"prettier": "^3.7.3",
|
|
73
|
+
"react": "^19.2.1",
|
|
74
|
+
"react-dom": "^19.2.1",
|
|
74
75
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
75
76
|
"typescript": "^5.8.3",
|
|
76
77
|
"vite": "^6.3.4",
|
|
77
|
-
"vitest": "^3.
|
|
78
|
+
"vitest": "^3.2.4",
|
|
78
79
|
"@repo/config-eslint": "0.0.0",
|
|
79
80
|
"@repo/package.bundle": "3.82.0",
|
|
80
81
|
"@repo/config-test": "0.0.1",
|
|
@@ -6,6 +6,13 @@ export {SanityApp, type SanityAppProps} from '../components/SanityApp'
|
|
|
6
6
|
export {SDKProvider, type SDKProviderProps} from '../components/SDKProvider'
|
|
7
7
|
export {ComlinkTokenRefreshProvider} from '../context/ComlinkTokenRefresh'
|
|
8
8
|
export {ResourceProvider, type ResourceProviderProps} from '../context/ResourceProvider'
|
|
9
|
+
export {
|
|
10
|
+
useAgentGenerate,
|
|
11
|
+
useAgentPatch,
|
|
12
|
+
useAgentPrompt,
|
|
13
|
+
useAgentTransform,
|
|
14
|
+
useAgentTranslate,
|
|
15
|
+
} from '../hooks/agent/agentActions'
|
|
9
16
|
export {useAuthState} from '../hooks/auth/useAuthState'
|
|
10
17
|
export {useAuthToken} from '../hooks/auth/useAuthToken'
|
|
11
18
|
export {useCurrentUser} from '../hooks/auth/useCurrentUser'
|
|
@@ -29,6 +36,7 @@ export {
|
|
|
29
36
|
} from '../hooks/comlink/useWindowConnection'
|
|
30
37
|
export {useSanityInstance} from '../hooks/context/useSanityInstance'
|
|
31
38
|
export {useDashboardNavigate} from '../hooks/dashboard/useDashboardNavigate'
|
|
39
|
+
export {useDispatchIntent} from '../hooks/dashboard/useDispatchIntent'
|
|
32
40
|
export {useManageFavorite} from '../hooks/dashboard/useManageFavorite'
|
|
33
41
|
export {
|
|
34
42
|
type NavigateToStudioResult,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import {ClientError} from '@sanity/client'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AuthStateType,
|
|
4
|
+
getClientErrorApiBody,
|
|
5
|
+
getClientErrorApiDescription,
|
|
6
|
+
isProjectUserNotFoundClientError,
|
|
7
|
+
} from '@sanity/sdk'
|
|
3
8
|
import {useCallback, useEffect, useState} from 'react'
|
|
4
9
|
import {type FallbackProps} from 'react-error-boundary'
|
|
5
10
|
|
|
@@ -35,6 +40,7 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
|
|
|
35
40
|
const [authErrorMessage, setAuthErrorMessage] = useState(
|
|
36
41
|
'Please try again or contact support if the problem persists.',
|
|
37
42
|
)
|
|
43
|
+
const [showRetryCta, setShowRetryCta] = useState(true)
|
|
38
44
|
|
|
39
45
|
const handleRetry = useCallback(async () => {
|
|
40
46
|
await logout()
|
|
@@ -44,18 +50,28 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
|
|
|
44
50
|
useEffect(() => {
|
|
45
51
|
if (error instanceof ClientError) {
|
|
46
52
|
if (error.statusCode === 401) {
|
|
47
|
-
|
|
53
|
+
// Surface a friendly message for projectUserNotFoundError (do not logout/refresh)
|
|
54
|
+
if (isProjectUserNotFoundClientError(error)) {
|
|
55
|
+
const description = getClientErrorApiDescription(error)
|
|
56
|
+
if (description) setAuthErrorMessage(description)
|
|
57
|
+
setShowRetryCta(false)
|
|
58
|
+
} else {
|
|
59
|
+
setShowRetryCta(true)
|
|
60
|
+
handleRetry()
|
|
61
|
+
}
|
|
48
62
|
} else if (error.statusCode === 404) {
|
|
49
|
-
const errorMessage = error
|
|
63
|
+
const errorMessage = getClientErrorApiBody(error)?.message || ''
|
|
50
64
|
if (errorMessage.startsWith('Session with sid') && errorMessage.endsWith('not found')) {
|
|
51
65
|
setAuthErrorMessage('The session ID is invalid or expired.')
|
|
52
66
|
} else {
|
|
53
67
|
setAuthErrorMessage('The login link is invalid or expired. Please try again.')
|
|
54
68
|
}
|
|
69
|
+
setShowRetryCta(true)
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
72
|
if (authState.type !== AuthStateType.ERROR && error instanceof ConfigurationError) {
|
|
58
73
|
setAuthErrorMessage(error.message)
|
|
74
|
+
setShowRetryCta(true)
|
|
59
75
|
}
|
|
60
76
|
}, [authState, handleRetry, error])
|
|
61
77
|
|
|
@@ -63,10 +79,14 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
|
|
|
63
79
|
<Error
|
|
64
80
|
heading={error instanceof AuthError ? 'Authentication Error' : 'Configuration Error'}
|
|
65
81
|
description={authErrorMessage}
|
|
66
|
-
cta={
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
cta={
|
|
83
|
+
showRetryCta
|
|
84
|
+
? {
|
|
85
|
+
text: 'Retry',
|
|
86
|
+
onClick: handleRetry,
|
|
87
|
+
}
|
|
88
|
+
: undefined
|
|
89
|
+
}
|
|
70
90
|
/>
|
|
71
91
|
)
|
|
72
92
|
}
|
|
@@ -5,7 +5,6 @@ import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest
|
|
|
5
5
|
|
|
6
6
|
import {useAuthState} from '../hooks/auth/useAuthState'
|
|
7
7
|
import {useWindowConnection} from '../hooks/comlink/useWindowConnection'
|
|
8
|
-
import {useSanityInstance} from '../hooks/context/useSanityInstance'
|
|
9
8
|
import {ComlinkTokenRefreshProvider} from './ComlinkTokenRefresh'
|
|
10
9
|
import {ResourceProvider} from './ResourceProvider'
|
|
11
10
|
|
|
@@ -27,24 +26,13 @@ vi.mock('../hooks/comlink/useWindowConnection', () => ({
|
|
|
27
26
|
useWindowConnection: vi.fn(),
|
|
28
27
|
}))
|
|
29
28
|
|
|
30
|
-
vi.mock('../hooks/context/useSanityInstance', () => ({
|
|
31
|
-
useSanityInstance: vi.fn(),
|
|
32
|
-
}))
|
|
33
|
-
|
|
34
29
|
// Use simpler mock typings
|
|
35
30
|
const mockGetIsInDashboardState = getIsInDashboardState as Mock
|
|
36
31
|
const mockSetAuthToken = setAuthToken as Mock
|
|
37
32
|
const mockUseAuthState = useAuthState as Mock
|
|
38
33
|
const mockUseWindowConnection = useWindowConnection as Mock
|
|
39
|
-
const mockUseSanityInstance = useSanityInstance as unknown as Mock
|
|
40
34
|
|
|
41
35
|
const mockFetch = vi.fn()
|
|
42
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
-
const mockSanityInstance: any = {
|
|
44
|
-
projectId: 'test',
|
|
45
|
-
dataset: 'test',
|
|
46
|
-
config: {studioMode: {enabled: false}},
|
|
47
|
-
}
|
|
48
36
|
|
|
49
37
|
describe('ComlinkTokenRefresh', () => {
|
|
50
38
|
beforeEach(() => {
|
|
@@ -52,7 +40,6 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
52
40
|
mockGetIsInDashboardState.mockReturnValue({getCurrent: vi.fn(() => false)})
|
|
53
41
|
mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
|
|
54
42
|
mockUseWindowConnection.mockReturnValue({fetch: mockFetch})
|
|
55
|
-
mockUseSanityInstance.mockReturnValue(mockSanityInstance)
|
|
56
43
|
})
|
|
57
44
|
|
|
58
45
|
afterEach(() => {
|
|
@@ -127,6 +114,15 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
127
114
|
})
|
|
128
115
|
mockFetch.mockResolvedValueOnce({token: 'new-token'})
|
|
129
116
|
|
|
117
|
+
// Insert an Unauthorized error container that should be removed on success
|
|
118
|
+
const errorContainer = document.createElement('div')
|
|
119
|
+
errorContainer.id = '__sanityError'
|
|
120
|
+
const child = document.createElement('div')
|
|
121
|
+
child.textContent =
|
|
122
|
+
'Uncaught error: Unauthorized - A valid session is required for this endpoint'
|
|
123
|
+
errorContainer.appendChild(child)
|
|
124
|
+
document.body.appendChild(errorContainer)
|
|
125
|
+
|
|
130
126
|
render(
|
|
131
127
|
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
132
128
|
<ComlinkTokenRefreshProvider>
|
|
@@ -141,6 +137,14 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
141
137
|
|
|
142
138
|
expect(mockSetAuthToken).toHaveBeenCalledWith(expect.any(Object), 'new-token')
|
|
143
139
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
140
|
+
expect(mockFetch).toHaveBeenCalledWith('dashboard/v1/auth/tokens/create')
|
|
141
|
+
// Assert setAuthToken was called with instance matching provider config
|
|
142
|
+
const instanceArg = mockSetAuthToken.mock.calls[0][0]
|
|
143
|
+
expect(instanceArg.config).toEqual(
|
|
144
|
+
expect.objectContaining({projectId: 'test-project', dataset: 'test-dataset'}),
|
|
145
|
+
)
|
|
146
|
+
// Unauthorized error container should be removed
|
|
147
|
+
expect(document.getElementById('__sanityError')).toBeNull()
|
|
144
148
|
})
|
|
145
149
|
|
|
146
150
|
it('should not set auth token if received token is null when not in studio mode', async () => {
|
|
@@ -243,20 +247,100 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
243
247
|
expect(mockFetch).toHaveBeenCalledWith('dashboard/v1/auth/tokens/create')
|
|
244
248
|
})
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
...mockSanityInstance,
|
|
251
|
-
config: {studioMode: {enabled: true}},
|
|
252
|
-
})
|
|
250
|
+
it('dedupes multiple 401 errors while a refresh is in progress', async () => {
|
|
251
|
+
mockUseAuthState.mockReturnValue({
|
|
252
|
+
type: AuthStateType.ERROR,
|
|
253
|
+
error: {statusCode: 401, message: 'Unauthorized'},
|
|
253
254
|
})
|
|
255
|
+
// Return a promise we resolve later to keep in-progress true for a bit
|
|
256
|
+
let resolveFetch: (v: {token: string | null}) => void
|
|
257
|
+
mockFetch.mockImplementation(
|
|
258
|
+
() =>
|
|
259
|
+
new Promise<{token: string | null}>((resolve) => {
|
|
260
|
+
resolveFetch = resolve
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
254
263
|
|
|
255
|
-
|
|
256
|
-
|
|
264
|
+
const {rerender} = render(
|
|
265
|
+
<ResourceProvider fallback={null}>
|
|
266
|
+
<ComlinkTokenRefreshProvider>
|
|
267
|
+
<div>Test</div>
|
|
268
|
+
</ComlinkTokenRefreshProvider>
|
|
269
|
+
</ResourceProvider>,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
// Trigger a second 401 while the first request is still in progress
|
|
273
|
+
mockUseAuthState.mockReturnValue({
|
|
274
|
+
type: AuthStateType.ERROR,
|
|
275
|
+
error: {statusCode: 401, message: 'Unauthorized again'},
|
|
276
|
+
})
|
|
277
|
+
act(() => {
|
|
278
|
+
rerender(
|
|
279
|
+
<ResourceProvider fallback={null}>
|
|
280
|
+
<ComlinkTokenRefreshProvider>
|
|
281
|
+
<div>Test</div>
|
|
282
|
+
</ComlinkTokenRefreshProvider>
|
|
283
|
+
</ResourceProvider>,
|
|
284
|
+
)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// Only one fetch should be in-flight
|
|
288
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
289
|
+
|
|
290
|
+
// Finish the first fetch
|
|
291
|
+
await act(async () => {
|
|
292
|
+
resolveFetch!({token: null})
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('requests again after timeout if previous request did not resolve', async () => {
|
|
297
|
+
mockUseAuthState.mockReturnValue({
|
|
298
|
+
type: AuthStateType.ERROR,
|
|
299
|
+
error: {statusCode: 401, message: 'Unauthorized'},
|
|
300
|
+
})
|
|
301
|
+
// First call never resolves
|
|
302
|
+
mockFetch.mockImplementationOnce(() => new Promise(() => {}))
|
|
303
|
+
|
|
304
|
+
const {rerender} = render(
|
|
305
|
+
<ResourceProvider fallback={null}>
|
|
257
306
|
<ComlinkTokenRefreshProvider>
|
|
258
307
|
<div>Test</div>
|
|
259
|
-
</ComlinkTokenRefreshProvider
|
|
308
|
+
</ComlinkTokenRefreshProvider>
|
|
309
|
+
</ResourceProvider>,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
313
|
+
|
|
314
|
+
// After timeout elapses, a subsequent 401 should trigger another fetch
|
|
315
|
+
await act(async () => {
|
|
316
|
+
await vi.advanceTimersByTimeAsync(10000)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
mockUseAuthState.mockReturnValue({
|
|
320
|
+
type: AuthStateType.ERROR,
|
|
321
|
+
error: {statusCode: 401, message: 'Unauthorized again'},
|
|
322
|
+
})
|
|
323
|
+
act(() => {
|
|
324
|
+
rerender(
|
|
325
|
+
<ResourceProvider fallback={null}>
|
|
326
|
+
<ComlinkTokenRefreshProvider>
|
|
327
|
+
<div>Test</div>
|
|
328
|
+
</ComlinkTokenRefreshProvider>
|
|
329
|
+
</ResourceProvider>,
|
|
330
|
+
)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
expect(mockFetch).toHaveBeenCalledTimes(2)
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('when in studio mode', () => {
|
|
337
|
+
it('should not render DashboardTokenRefresh when studio mode enabled', () => {
|
|
338
|
+
render(
|
|
339
|
+
<ResourceProvider fallback={null} studioMode={{enabled: true}}>
|
|
340
|
+
<ComlinkTokenRefreshProvider>
|
|
341
|
+
<div>Test</div>
|
|
342
|
+
</ComlinkTokenRefreshProvider>
|
|
343
|
+
</ResourceProvider>,
|
|
260
344
|
)
|
|
261
345
|
|
|
262
346
|
// In studio mode, provider should return children directly
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import {renderHook} from '@testing-library/react'
|
|
3
|
+
import {of} from 'rxjs'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
7
|
+
import {
|
|
8
|
+
useAgentGenerate,
|
|
9
|
+
useAgentPatch,
|
|
10
|
+
useAgentPrompt,
|
|
11
|
+
useAgentTransform,
|
|
12
|
+
useAgentTranslate,
|
|
13
|
+
} from './agentActions'
|
|
14
|
+
|
|
15
|
+
vi.mock('@sanity/sdk', async (orig) => {
|
|
16
|
+
const actual = await orig()
|
|
17
|
+
return {
|
|
18
|
+
...(actual as Record<string, any>),
|
|
19
|
+
agentGenerate: vi.fn(() => of('gen')),
|
|
20
|
+
agentTransform: vi.fn(() => of('xform')),
|
|
21
|
+
agentTranslate: vi.fn(() => of('xlate')),
|
|
22
|
+
agentPrompt: vi.fn(() => of('prompted')),
|
|
23
|
+
agentPatch: vi.fn(() => of('patched')),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('agent action hooks', () => {
|
|
28
|
+
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
29
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
30
|
+
{children}
|
|
31
|
+
</ResourceProvider>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
it('useAgentGenerate returns a callable that delegates to core', async () => {
|
|
35
|
+
const {result} = renderHook(() => useAgentGenerate(), {wrapper})
|
|
36
|
+
const value = await new Promise<any>((resolve, reject) => {
|
|
37
|
+
result.current({} as any).subscribe({
|
|
38
|
+
next: (v) => resolve(v),
|
|
39
|
+
error: reject,
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
expect(value).toBe('gen')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('useAgentTransform returns a callable that delegates to core', async () => {
|
|
46
|
+
const {result} = renderHook(() => useAgentTransform(), {wrapper})
|
|
47
|
+
const value = await new Promise<any>((resolve, reject) => {
|
|
48
|
+
result.current({} as any).subscribe({
|
|
49
|
+
next: (v) => resolve(v),
|
|
50
|
+
error: reject,
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
expect(value).toBe('xform')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('useAgentTranslate returns a callable that delegates to core', async () => {
|
|
57
|
+
const {result} = renderHook(() => useAgentTranslate(), {wrapper})
|
|
58
|
+
const value = await new Promise<any>((resolve, reject) => {
|
|
59
|
+
result.current({} as any).subscribe({
|
|
60
|
+
next: (v) => resolve(v),
|
|
61
|
+
error: reject,
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
expect(value).toBe('xlate')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('useAgentPrompt returns a callable that delegates to core', async () => {
|
|
68
|
+
const {result} = renderHook(() => useAgentPrompt(), {wrapper})
|
|
69
|
+
const value = await result.current({} as any)
|
|
70
|
+
expect(value).toBe('prompted')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('useAgentPatch returns a callable that delegates to core', async () => {
|
|
74
|
+
const {result} = renderHook(() => useAgentPatch(), {wrapper})
|
|
75
|
+
const value = await result.current({} as any)
|
|
76
|
+
expect(value).toBe('patched')
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentGenerate,
|
|
3
|
+
type AgentGenerateOptions,
|
|
4
|
+
agentPatch,
|
|
5
|
+
type AgentPatchOptions,
|
|
6
|
+
type AgentPatchResult,
|
|
7
|
+
agentPrompt,
|
|
8
|
+
type AgentPromptOptions,
|
|
9
|
+
type AgentPromptResult,
|
|
10
|
+
agentTransform,
|
|
11
|
+
type AgentTransformOptions,
|
|
12
|
+
agentTranslate,
|
|
13
|
+
type AgentTranslateOptions,
|
|
14
|
+
type SanityInstance,
|
|
15
|
+
} from '@sanity/sdk'
|
|
16
|
+
import {firstValueFrom} from 'rxjs'
|
|
17
|
+
|
|
18
|
+
import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
19
|
+
|
|
20
|
+
interface Subscription {
|
|
21
|
+
unsubscribe(): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Observer<T> {
|
|
25
|
+
next?: (value: T) => void
|
|
26
|
+
error?: (err: unknown) => void
|
|
27
|
+
complete?: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Subscribable<T> {
|
|
31
|
+
subscribe(observer: Observer<T>): Subscription
|
|
32
|
+
subscribe(
|
|
33
|
+
next: (value: T) => void,
|
|
34
|
+
error?: (err: unknown) => void,
|
|
35
|
+
complete?: () => void,
|
|
36
|
+
): Subscription
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @alpha
|
|
41
|
+
* Generates content for a document (or specific fields) via Sanity Agent Actions.
|
|
42
|
+
* - Uses instruction templates with `$variables` and supports `instructionParams` (constants, fields, documents, GROQ queries).
|
|
43
|
+
* - Can target specific paths/fields; supports image generation when targeting image fields.
|
|
44
|
+
* - Supports optional `temperature`, `async`, `noWrite`, and `conditionalPaths`.
|
|
45
|
+
*
|
|
46
|
+
* Returns a stable callback that triggers the action and yields a Subscribable stream.
|
|
47
|
+
*/
|
|
48
|
+
export const useAgentGenerate: () => (options: AgentGenerateOptions) => Subscribable<unknown> =
|
|
49
|
+
createCallbackHook(agentGenerate) as unknown as () => (
|
|
50
|
+
options: AgentGenerateOptions,
|
|
51
|
+
) => Subscribable<unknown>
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @alpha
|
|
55
|
+
* Transforms an existing document or selected fields using Sanity Agent Actions.
|
|
56
|
+
* - Accepts `instruction` and `instructionParams` (constants, fields, documents, GROQ queries).
|
|
57
|
+
* - Can write to the same or a different `targetDocument` (create/edit), and target specific paths.
|
|
58
|
+
* - Supports per-path image transform instructions and image description operations.
|
|
59
|
+
* - Optional `temperature`, `async`, `noWrite`, `conditionalPaths`.
|
|
60
|
+
*
|
|
61
|
+
* Returns a stable callback that triggers the action and yields a Subscribable stream.
|
|
62
|
+
*/
|
|
63
|
+
export const useAgentTransform: () => (options: AgentTransformOptions) => Subscribable<unknown> =
|
|
64
|
+
createCallbackHook(agentTransform) as unknown as () => (
|
|
65
|
+
options: AgentTransformOptions,
|
|
66
|
+
) => Subscribable<unknown>
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @alpha
|
|
70
|
+
* Translates documents or fields using Sanity Agent Actions.
|
|
71
|
+
* - Configure `fromLanguage`/`toLanguage`, optional `styleGuide`, and `protectedPhrases`.
|
|
72
|
+
* - Can write into a different `targetDocument`, and/or store language in a field.
|
|
73
|
+
* - Optional `temperature`, `async`, `noWrite`, `conditionalPaths`.
|
|
74
|
+
*
|
|
75
|
+
* Returns a stable callback that triggers the action and yields a Subscribable stream.
|
|
76
|
+
*/
|
|
77
|
+
export const useAgentTranslate: () => (options: AgentTranslateOptions) => Subscribable<unknown> =
|
|
78
|
+
createCallbackHook(agentTranslate) as unknown as () => (
|
|
79
|
+
options: AgentTranslateOptions,
|
|
80
|
+
) => Subscribable<unknown>
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @alpha
|
|
84
|
+
* Prompts the LLM using the same instruction template format as other actions.
|
|
85
|
+
* - `format`: 'string' or 'json' (instruction must contain the word "json" for JSON responses).
|
|
86
|
+
* - Optional `temperature`.
|
|
87
|
+
*
|
|
88
|
+
* Returns a stable callback that triggers the action and resolves a Promise with the prompt result.
|
|
89
|
+
*/
|
|
90
|
+
function promptAdapter(
|
|
91
|
+
instance: SanityInstance,
|
|
92
|
+
options: AgentPromptOptions,
|
|
93
|
+
): Promise<AgentPromptResult> {
|
|
94
|
+
return firstValueFrom(agentPrompt(instance, options))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @alpha
|
|
99
|
+
* Prompts the LLM using the same instruction template format as other actions.
|
|
100
|
+
* - `format`: 'string' or 'json' (instruction must contain the word "json" for JSON responses).
|
|
101
|
+
* - Optional `temperature`.
|
|
102
|
+
*
|
|
103
|
+
* Returns a stable callback that triggers the action and resolves a Promise with the prompt result.
|
|
104
|
+
*/
|
|
105
|
+
export const useAgentPrompt: () => (options: AgentPromptOptions) => Promise<AgentPromptResult> =
|
|
106
|
+
createCallbackHook(promptAdapter)
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @alpha
|
|
110
|
+
* Schema-aware patching with Sanity Agent Actions.
|
|
111
|
+
* - Validates provided paths/values against the document schema and merges object values safely.
|
|
112
|
+
* - Prevents duplicate keys and supports array appends (including after a specific keyed item).
|
|
113
|
+
* - Accepts `documentId` or `targetDocument` (mutually exclusive).
|
|
114
|
+
* - Optional `async`, `noWrite`, `conditionalPaths`.
|
|
115
|
+
*
|
|
116
|
+
* Returns a stable callback that triggers the action and resolves a Promise with the patch result.
|
|
117
|
+
*/
|
|
118
|
+
function patchAdapter(
|
|
119
|
+
instance: SanityInstance,
|
|
120
|
+
options: AgentPatchOptions,
|
|
121
|
+
): Promise<AgentPatchResult> {
|
|
122
|
+
return firstValueFrom(agentPatch(instance, options))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @alpha
|
|
127
|
+
* Schema-aware patching with Sanity Agent Actions.
|
|
128
|
+
* - Validates provided paths/values against the document schema and merges object values safely.
|
|
129
|
+
* - Prevents duplicate keys and supports array appends (including after a specific keyed item).
|
|
130
|
+
* - Accepts `documentId` or `targetDocument` (mutually exclusive).
|
|
131
|
+
* - Optional `async`, `noWrite`, `conditionalPaths`.
|
|
132
|
+
*
|
|
133
|
+
* Returns a stable callback that triggers the action and resolves a Promise with the patch result.
|
|
134
|
+
*/
|
|
135
|
+
export const useAgentPatch: () => (options: AgentPatchOptions) => Promise<AgentPatchResult> =
|
|
136
|
+
createCallbackHook(patchAdapter)
|
|
@@ -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
|
+
}
|