@sanity/sdk-react 2.0.2 → 2.1.1
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 +59 -0
- package/dist/index.js +52 -11
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/_exports/sdk-react.ts +2 -0
- package/src/components/SanityApp.test.tsx +42 -0
- package/src/components/SanityApp.tsx +3 -2
- package/src/context/ResourceProvider.test.tsx +9 -5
- package/src/hooks/dashboard/useManageFavorite.test.ts +2 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +2 -0
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +2 -0
- package/src/hooks/presence/usePresence.test.tsx +80 -0
- package/src/hooks/presence/usePresence.ts +23 -0
- package/src/hooks/users/useUser.test.tsx +387 -0
- package/src/hooks/users/useUser.ts +109 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"@types/lodash-es": "^4.17.12",
|
|
49
49
|
"groq": "3.88.1-typegen-experimental.0",
|
|
50
50
|
"lodash-es": "^4.17.21",
|
|
51
|
-
"react-compiler-runtime": "19.1.0-rc.
|
|
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.1.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@sanity/browserslist-config": "^1.0.5",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
"typescript": "^5.8.3",
|
|
75
75
|
"vite": "^6.3.4",
|
|
76
76
|
"vitest": "^3.1.2",
|
|
77
|
-
"@repo/config-eslint": "0.0.0",
|
|
78
77
|
"@repo/config-test": "0.0.1",
|
|
79
|
-
"@repo/tsconfig": "0.0.1",
|
|
80
78
|
"@repo/package.bundle": "3.82.0",
|
|
81
|
-
"@repo/package.config": "0.0.1"
|
|
79
|
+
"@repo/package.config": "0.0.1",
|
|
80
|
+
"@repo/tsconfig": "0.0.1",
|
|
81
|
+
"@repo/config-eslint": "0.0.0"
|
|
82
82
|
},
|
|
83
83
|
"peerDependencies": {
|
|
84
84
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -53,6 +53,7 @@ export {
|
|
|
53
53
|
type PaginatedDocumentsResponse,
|
|
54
54
|
usePaginatedDocuments,
|
|
55
55
|
} from '../hooks/paginatedDocuments/usePaginatedDocuments'
|
|
56
|
+
export {usePresence} from '../hooks/presence/usePresence'
|
|
56
57
|
export {
|
|
57
58
|
useDocumentPreview,
|
|
58
59
|
type useDocumentPreviewOptions,
|
|
@@ -68,6 +69,7 @@ export {type ProjectWithoutMembers, useProjects} from '../hooks/projects/useProj
|
|
|
68
69
|
export {useQuery} from '../hooks/query/useQuery'
|
|
69
70
|
export {useActiveReleases} from '../hooks/releases/useActiveReleases'
|
|
70
71
|
export {usePerspective} from '../hooks/releases/usePerspective'
|
|
72
|
+
export {type UserResult, useUser} from '../hooks/users/useUser'
|
|
71
73
|
export {type UsersResult, useUsers} from '../hooks/users/useUsers'
|
|
72
74
|
export {REACT_SDK_VERSION} from '../version'
|
|
73
75
|
export {type DatasetsResponse, type SanityProjectMember} from '@sanity/client'
|
|
@@ -170,6 +170,7 @@ describe('SanityApp', () => {
|
|
|
170
170
|
|
|
171
171
|
it('redirects to core if not inside iframe and not local url', async () => {
|
|
172
172
|
const originalLocation = window.location
|
|
173
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
173
174
|
|
|
174
175
|
const mockLocation = {
|
|
175
176
|
replace: vi.fn(),
|
|
@@ -203,6 +204,7 @@ describe('SanityApp', () => {
|
|
|
203
204
|
value: originalLocation,
|
|
204
205
|
writable: true,
|
|
205
206
|
})
|
|
207
|
+
consoleWarnSpy.mockRestore()
|
|
206
208
|
})
|
|
207
209
|
|
|
208
210
|
it('does not redirect to core if not inside iframe and local url', async () => {
|
|
@@ -241,4 +243,44 @@ describe('SanityApp', () => {
|
|
|
241
243
|
writable: true,
|
|
242
244
|
})
|
|
243
245
|
})
|
|
246
|
+
|
|
247
|
+
it('does not redirect to core if studioMode is enabled', async () => {
|
|
248
|
+
const originalLocation = window.location
|
|
249
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
250
|
+
|
|
251
|
+
const mockLocation = {
|
|
252
|
+
replace: vi.fn(),
|
|
253
|
+
href: 'http://sanity-test.app',
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const mockSanityConfig: SanityConfig = {
|
|
257
|
+
projectId: 'test-project',
|
|
258
|
+
dataset: 'test-dataset',
|
|
259
|
+
studioMode: {enabled: true},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
Object.defineProperty(window, 'location', {
|
|
263
|
+
value: mockLocation,
|
|
264
|
+
writable: true,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
render(
|
|
268
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
269
|
+
<div>Test Child</div>
|
|
270
|
+
</SanityApp>,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
// Wait for 1 second
|
|
274
|
+
await new Promise((resolve) => setTimeout(resolve, 1010))
|
|
275
|
+
|
|
276
|
+
// Add assertions based on your iframe-specific behavior
|
|
277
|
+
expect(mockLocation.replace).not.toHaveBeenCalled()
|
|
278
|
+
|
|
279
|
+
// Clean up the mock
|
|
280
|
+
Object.defineProperty(window, 'location', {
|
|
281
|
+
value: originalLocation,
|
|
282
|
+
writable: true,
|
|
283
|
+
})
|
|
284
|
+
consoleWarnSpy.mockRestore()
|
|
285
|
+
})
|
|
244
286
|
})
|
|
@@ -86,8 +86,9 @@ export function SanityApp({
|
|
|
86
86
|
}: SanityAppProps): ReactElement {
|
|
87
87
|
useEffect(() => {
|
|
88
88
|
let timeout: NodeJS.Timeout | undefined
|
|
89
|
+
const primaryConfig = Array.isArray(config) ? config[0] : config
|
|
89
90
|
|
|
90
|
-
if (!isInIframe() && !isLocalUrl(window)) {
|
|
91
|
+
if (!isInIframe() && !isLocalUrl(window) && !primaryConfig?.studioMode?.enabled) {
|
|
91
92
|
// If the app is not running in an iframe and is not a local url, redirect to core.
|
|
92
93
|
timeout = setTimeout(() => {
|
|
93
94
|
// eslint-disable-next-line no-console
|
|
@@ -96,7 +97,7 @@ export function SanityApp({
|
|
|
96
97
|
}, 1000)
|
|
97
98
|
}
|
|
98
99
|
return () => clearTimeout(timeout)
|
|
99
|
-
}, [])
|
|
100
|
+
}, [config])
|
|
100
101
|
|
|
101
102
|
return (
|
|
102
103
|
<SDKProvider {...props} fallback={fallback} config={config}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
|
-
import {render, screen} from '@testing-library/react'
|
|
2
|
+
import {act, render, screen} from '@testing-library/react'
|
|
3
3
|
import {StrictMode, use, useEffect} from 'react'
|
|
4
4
|
import {describe, expect, it} from 'vitest'
|
|
5
5
|
|
|
@@ -36,7 +36,7 @@ describe('ResourceProvider', () => {
|
|
|
36
36
|
expect(screen.getByTestId('test-child')).toBeInTheDocument()
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
it('shows fallback during loading', () => {
|
|
39
|
+
it('shows fallback during loading', async () => {
|
|
40
40
|
const {promise, resolve} = promiseWithResolvers()
|
|
41
41
|
function SuspendingChild(): React.ReactNode {
|
|
42
42
|
throw promise
|
|
@@ -49,7 +49,9 @@ describe('ResourceProvider', () => {
|
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
expect(screen.getByTestId('fallback')).toBeInTheDocument()
|
|
52
|
-
|
|
52
|
+
act(() => {
|
|
53
|
+
resolve()
|
|
54
|
+
})
|
|
53
55
|
})
|
|
54
56
|
|
|
55
57
|
it('creates root instance when no parent context exists', async () => {
|
|
@@ -138,7 +140,7 @@ describe('ResourceProvider', () => {
|
|
|
138
140
|
expect(instance?.isDisposed()).toBe(false)
|
|
139
141
|
})
|
|
140
142
|
|
|
141
|
-
it('uses default fallback when none provided', () => {
|
|
143
|
+
it('uses default fallback when none provided', async () => {
|
|
142
144
|
const {promise, resolve} = promiseWithResolvers()
|
|
143
145
|
function SuspendingChild(): React.ReactNode {
|
|
144
146
|
throw promise
|
|
@@ -152,6 +154,8 @@ describe('ResourceProvider', () => {
|
|
|
152
154
|
)
|
|
153
155
|
|
|
154
156
|
expect(screen.getByText(/Warning: No fallback provided/)).toBeInTheDocument()
|
|
155
|
-
|
|
157
|
+
act(() => {
|
|
158
|
+
resolve()
|
|
159
|
+
})
|
|
156
160
|
})
|
|
157
161
|
})
|
|
@@ -172,6 +172,7 @@ describe('useManageFavorite', () => {
|
|
|
172
172
|
|
|
173
173
|
it('should throw error during favorite/unfavorite actions', async () => {
|
|
174
174
|
const errorMessage = 'Failed to update favorite status'
|
|
175
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
175
176
|
|
|
176
177
|
mockFetch.mockImplementation(() => {
|
|
177
178
|
throw new Error(errorMessage)
|
|
@@ -191,6 +192,7 @@ describe('useManageFavorite', () => {
|
|
|
191
192
|
})
|
|
192
193
|
|
|
193
194
|
expect(resolveFavoritesState).not.toHaveBeenCalled()
|
|
195
|
+
consoleErrorSpy.mockRestore()
|
|
194
196
|
})
|
|
195
197
|
|
|
196
198
|
it('should throw error when studio resource is missing projectId or dataset', () => {
|
|
@@ -75,10 +75,12 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
it('does not send message when no workspace is found', () => {
|
|
78
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
78
79
|
mockWorkspacesByProjectIdAndDataset = {}
|
|
79
80
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
80
81
|
result.current.navigateToStudioDocument()
|
|
81
82
|
expect(mockSendMessage).not.toHaveBeenCalled()
|
|
83
|
+
consoleSpy.mockRestore()
|
|
82
84
|
})
|
|
83
85
|
|
|
84
86
|
it('warns when multiple workspaces are found', () => {
|
|
@@ -45,6 +45,7 @@ describe('useRecordDocumentHistoryEvent', () => {
|
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
it('should handle errors when sending messages', () => {
|
|
48
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
48
49
|
mockSendMessage.mockImplementation(() => {
|
|
49
50
|
throw new Error('Failed to send message')
|
|
50
51
|
})
|
|
@@ -52,6 +53,7 @@ describe('useRecordDocumentHistoryEvent', () => {
|
|
|
52
53
|
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
53
54
|
|
|
54
55
|
expect(() => result.current.recordEvent('viewed')).toThrow('Failed to send message')
|
|
56
|
+
consoleErrorSpy.mockRestore()
|
|
55
57
|
})
|
|
56
58
|
|
|
57
59
|
it('should throw error when resourceId is missing for non-studio resources', () => {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {getPresence, type SanityUser, type UserPresence} from '@sanity/sdk'
|
|
2
|
+
import {NEVER} from 'rxjs'
|
|
3
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {act, renderHook} from '../../../test/test-utils'
|
|
6
|
+
import {usePresence} from './usePresence'
|
|
7
|
+
|
|
8
|
+
vi.mock('@sanity/sdk', () => ({
|
|
9
|
+
getPresence: vi.fn(),
|
|
10
|
+
createSanityInstance: vi.fn(() => ({
|
|
11
|
+
createChild: vi.fn(),
|
|
12
|
+
isDisposed: vi.fn(() => false),
|
|
13
|
+
dispose: vi.fn(),
|
|
14
|
+
})),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
18
|
+
useSanityInstance: vi.fn(() => ({config: {projectId: 'test', dataset: 'test'}})),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
describe('usePresence', () => {
|
|
22
|
+
it('should return presence locations and update when the store changes', () => {
|
|
23
|
+
const initialLocations: UserPresence[] = [
|
|
24
|
+
{
|
|
25
|
+
user: {
|
|
26
|
+
sanityUserId: 'user1',
|
|
27
|
+
profile: undefined,
|
|
28
|
+
memberships: [],
|
|
29
|
+
} as unknown as SanityUser,
|
|
30
|
+
sessionId: 'session1',
|
|
31
|
+
state: 'online',
|
|
32
|
+
lastActiveAt: new Date().toISOString(),
|
|
33
|
+
},
|
|
34
|
+
] as unknown as UserPresence[]
|
|
35
|
+
const updatedLocations: UserPresence[] = [
|
|
36
|
+
...initialLocations,
|
|
37
|
+
{
|
|
38
|
+
user: {
|
|
39
|
+
sanityUserId: 'user2',
|
|
40
|
+
profile: undefined,
|
|
41
|
+
memberships: [],
|
|
42
|
+
} as unknown as SanityUser,
|
|
43
|
+
sessionId: 'session2',
|
|
44
|
+
state: 'online',
|
|
45
|
+
lastActiveAt: new Date().toISOString(),
|
|
46
|
+
},
|
|
47
|
+
] as unknown as UserPresence[]
|
|
48
|
+
|
|
49
|
+
let onStoreChange: () => void = () => {}
|
|
50
|
+
const getCurrent = vi.fn().mockReturnValue(initialLocations)
|
|
51
|
+
const mockPresenceSource = {
|
|
52
|
+
// It's called once for the server snapshot, and once for the client
|
|
53
|
+
getCurrent,
|
|
54
|
+
subscribe: vi.fn((callback) => {
|
|
55
|
+
onStoreChange = callback
|
|
56
|
+
// Return an unsubscribe function
|
|
57
|
+
return () => {}
|
|
58
|
+
}),
|
|
59
|
+
observable: NEVER,
|
|
60
|
+
}
|
|
61
|
+
vi.mocked(getPresence).mockReturnValue(mockPresenceSource)
|
|
62
|
+
|
|
63
|
+
const {result, unmount} = renderHook(() => usePresence())
|
|
64
|
+
|
|
65
|
+
// Initial state should be correct
|
|
66
|
+
expect(result.current.locations).toEqual(initialLocations)
|
|
67
|
+
expect(getCurrent).toHaveBeenCalled()
|
|
68
|
+
expect(mockPresenceSource.subscribe).toHaveBeenCalledTimes(1)
|
|
69
|
+
|
|
70
|
+
// Update state
|
|
71
|
+
getCurrent.mockReturnValue(updatedLocations)
|
|
72
|
+
act(() => {
|
|
73
|
+
onStoreChange()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// The hook should have been updated
|
|
77
|
+
expect(result.current.locations).toEqual(updatedLocations)
|
|
78
|
+
unmount()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {getPresence, type UserPresence} from '@sanity/sdk'
|
|
2
|
+
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A hook for subscribing to presence information for the current project.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export function usePresence(): {
|
|
11
|
+
locations: UserPresence[]
|
|
12
|
+
} {
|
|
13
|
+
const sanityInstance = useSanityInstance()
|
|
14
|
+
const source = useMemo(() => getPresence(sanityInstance), [sanityInstance])
|
|
15
|
+
const subscribe = useCallback((callback: () => void) => source.subscribe(callback), [source])
|
|
16
|
+
const locations = useSyncExternalStore(
|
|
17
|
+
subscribe,
|
|
18
|
+
() => source.getCurrent(),
|
|
19
|
+
() => source.getCurrent(),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return {locations: locations || []}
|
|
23
|
+
}
|