@sanity/sdk-react 2.7.0 → 3.0.0-rc.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 +125 -63
- package/dist/index.d.ts +381 -571
- package/dist/index.js +450 -366
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/_exports/index.ts +4 -0
- package/src/_exports/sdk-react.ts +16 -0
- package/src/components/SDKProvider.test.tsx +23 -58
- package/src/components/SDKProvider.tsx +38 -30
- package/src/components/SanityApp.test.tsx +12 -68
- package/src/components/SanityApp.tsx +88 -65
- package/src/components/auth/AuthBoundary.test.tsx +11 -26
- package/src/components/auth/LoginError.test.tsx +5 -0
- package/src/components/auth/LoginError.tsx +23 -2
- package/src/config/handles.ts +53 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +27 -10
- package/src/context/DefaultResourceContext.ts +10 -0
- package/src/context/PerspectiveContext.ts +12 -0
- package/src/context/ResourceProvider.test.tsx +99 -19
- package/src/context/ResourceProvider.tsx +103 -37
- package/src/context/ResourcesContext.tsx +7 -0
- package/src/context/SDKStudioContext.test.tsx +33 -28
- package/src/context/SDKStudioContext.ts +6 -0
- package/src/context/renderSanityApp.test.tsx +49 -151
- package/src/context/renderSanityApp.tsx +8 -12
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +56 -19
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +8 -2
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +32 -8
- package/src/hooks/client/useClient.test.tsx +4 -1
- package/src/hooks/client/useClient.ts +0 -1
- package/src/hooks/context/useDefaultResource.test.tsx +25 -0
- package/src/hooks/context/useDefaultResource.ts +30 -0
- package/src/hooks/context/useSanityInstance.test.tsx +2 -140
- package/src/hooks/context/useSanityInstance.ts +9 -53
- package/src/hooks/dashboard/useDispatchIntent.test.ts +24 -15
- package/src/hooks/dashboard/useDispatchIntent.ts +7 -7
- package/src/hooks/dashboard/useManageFavorite.test.tsx +34 -94
- package/src/hooks/dashboard/useManageFavorite.ts +16 -10
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +7 -5
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +6 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +2 -0
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -1
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +17 -38
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +12 -19
- package/src/hooks/datasets/useDatasets.test.ts +8 -22
- package/src/hooks/datasets/useDatasets.ts +8 -16
- package/src/hooks/document/useApplyDocumentActions.test.ts +98 -52
- package/src/hooks/document/useApplyDocumentActions.ts +35 -37
- package/src/hooks/document/useDocument.test.tsx +8 -37
- package/src/hooks/document/useDocument.ts +78 -129
- package/src/hooks/document/useDocumentEvent.test.tsx +7 -19
- package/src/hooks/document/useDocumentEvent.ts +21 -19
- package/src/hooks/document/useDocumentPermissions.test.tsx +75 -84
- package/src/hooks/document/useDocumentPermissions.ts +41 -28
- package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -3
- package/src/hooks/document/useDocumentSyncStatus.ts +19 -14
- package/src/hooks/document/useEditDocument.test.tsx +28 -70
- package/src/hooks/document/useEditDocument.ts +29 -149
- package/src/hooks/documents/useDocuments.test.tsx +44 -64
- package/src/hooks/documents/useDocuments.ts +19 -25
- package/src/hooks/helpers/createCallbackHook.test.tsx +19 -13
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -4
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +65 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +127 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +27 -34
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +19 -20
- package/src/hooks/presence/usePresence.test.tsx +71 -9
- package/src/hooks/presence/usePresence.ts +28 -3
- package/src/hooks/preview/useDocumentPreview.test.tsx +85 -193
- package/src/hooks/preview/useDocumentPreview.tsx +42 -62
- package/src/hooks/projection/useDocumentProjection.test.tsx +9 -37
- package/src/hooks/projection/useDocumentProjection.ts +9 -82
- package/src/hooks/projects/useProject.test.ts +1 -2
- package/src/hooks/projects/useProject.ts +7 -8
- package/src/hooks/query/useQuery.test.tsx +5 -6
- package/src/hooks/query/useQuery.ts +12 -91
- package/src/hooks/releases/useActiveReleases.test.tsx +2 -2
- package/src/hooks/releases/useActiveReleases.ts +25 -13
- package/src/hooks/releases/usePerspective.test.tsx +9 -17
- package/src/hooks/releases/usePerspective.ts +29 -18
- package/src/hooks/users/useUser.test.tsx +9 -3
- package/src/hooks/users/useUser.ts +1 -1
- package/src/hooks/users/useUsers.test.tsx +5 -2
- package/src/hooks/users/useUsers.ts +1 -1
- package/src/context/SourcesContext.tsx +0 -7
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -85
|
@@ -1,17 +1,42 @@
|
|
|
1
|
-
import {getPresence, type UserPresence} from '@sanity/sdk'
|
|
1
|
+
import {getPresence, isCanvasResource, isMediaLibraryResource, type UserPresence} from '@sanity/sdk'
|
|
2
2
|
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
3
3
|
|
|
4
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
4
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* A hook for subscribing to presence information for the current project.
|
|
8
10
|
* @public
|
|
9
11
|
*/
|
|
10
|
-
export function usePresence(): {
|
|
12
|
+
export function usePresence(options: ResourceHandle = {}): {
|
|
11
13
|
locations: UserPresence[]
|
|
12
14
|
} {
|
|
15
|
+
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
13
16
|
const sanityInstance = useSanityInstance()
|
|
14
|
-
|
|
17
|
+
|
|
18
|
+
// Validate resource type before attempting to create the presence store
|
|
19
|
+
// This provides immediate, clear feedback instead of hanging
|
|
20
|
+
if (normalizedOptions.resource) {
|
|
21
|
+
if (isMediaLibraryResource(normalizedOptions.resource)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'usePresence() does not support media library resources. Presence tracking requires a dataset resource. ' +
|
|
24
|
+
'Either remove the resourceName parameter or use a dataset resource instead.',
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isCanvasResource(normalizedOptions.resource)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'usePresence() does not support canvas resources. Presence tracking requires a dataset resource. ' +
|
|
31
|
+
'Either remove the resourceName parameter or use a dataset resource instead.',
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const source = useMemo(
|
|
37
|
+
() => getPresence(sanityInstance, normalizedOptions),
|
|
38
|
+
[sanityInstance, normalizedOptions],
|
|
39
|
+
)
|
|
15
40
|
const subscribe = useCallback((callback: () => void) => source.subscribe(callback), [source])
|
|
16
41
|
const locations = useSyncExternalStore(
|
|
17
42
|
subscribe,
|
|
@@ -1,233 +1,125 @@
|
|
|
1
|
-
import {type DocumentHandle,
|
|
2
|
-
import {
|
|
3
|
-
import {useRef} from 'react'
|
|
4
|
-
import {type Mock} from 'vitest'
|
|
1
|
+
import {type DocumentHandle, type PreviewQueryResult} from '@sanity/sdk'
|
|
2
|
+
import {beforeEach, describe, expect, test, vi} from 'vitest'
|
|
5
3
|
|
|
6
|
-
import {
|
|
4
|
+
import {render, renderHook, screen} from '../../../test/test-utils'
|
|
5
|
+
import {useDocumentProjection} from '../projection/useDocumentProjection'
|
|
7
6
|
import {useDocumentPreview} from './useDocumentPreview'
|
|
8
7
|
|
|
9
|
-
// Mock
|
|
10
|
-
|
|
11
|
-
let intersectionObserverCallback: (entries: IntersectionObserverEntry[]) => void
|
|
12
|
-
|
|
13
|
-
beforeAll(() => {
|
|
14
|
-
vi.stubGlobal(
|
|
15
|
-
'IntersectionObserver',
|
|
16
|
-
class {
|
|
17
|
-
constructor(callback: (entries: IntersectionObserverEntry[]) => void) {
|
|
18
|
-
intersectionObserverCallback = callback
|
|
19
|
-
mockIntersectionObserver(callback)
|
|
20
|
-
}
|
|
21
|
-
observe = vi.fn()
|
|
22
|
-
disconnect = vi.fn()
|
|
23
|
-
},
|
|
24
|
-
)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
// Mock the preview store
|
|
28
|
-
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
29
|
-
const actual = await importOriginal<typeof import('@sanity/sdk')>()
|
|
30
|
-
const getCurrent = vi.fn()
|
|
31
|
-
const subscribe = vi.fn()
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
...actual,
|
|
35
|
-
resolvePreview: vi.fn(),
|
|
36
|
-
getPreviewState: vi.fn().mockReturnValue({getCurrent, subscribe}),
|
|
37
|
-
}
|
|
38
|
-
})
|
|
8
|
+
// Mock useDocumentProjection since useDocumentPreview now uses it internally
|
|
9
|
+
vi.mock('../projection/useDocumentProjection')
|
|
39
10
|
|
|
40
11
|
const mockDocument: DocumentHandle = {
|
|
41
12
|
documentId: 'doc1',
|
|
42
13
|
documentType: 'exampleType',
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function TestComponent(docHandle: DocumentHandle) {
|
|
46
|
-
const ref = useRef(null)
|
|
47
|
-
const {data, isPending} = useDocumentPreview({...docHandle, ref})
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div ref={ref}>
|
|
51
|
-
<h1>{data?.title}</h1>
|
|
52
|
-
<p>{data?.subtitle}</p>
|
|
53
|
-
{isPending && <div>Pending...</div>}
|
|
54
|
-
</div>
|
|
55
|
-
)
|
|
14
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
56
15
|
}
|
|
57
16
|
|
|
58
17
|
describe('useDocumentPreview', () => {
|
|
59
|
-
let getCurrent: Mock
|
|
60
|
-
let subscribe: Mock
|
|
61
|
-
|
|
62
18
|
beforeEach(() => {
|
|
63
|
-
|
|
64
|
-
getCurrent = getPreviewState().getCurrent as Mock
|
|
65
|
-
// @ts-expect-error mock does not need param
|
|
66
|
-
subscribe = getPreviewState().subscribe as Mock
|
|
67
|
-
|
|
68
|
-
// Reset all mocks between tests
|
|
69
|
-
getCurrent.mockReset()
|
|
70
|
-
subscribe.mockReset()
|
|
71
|
-
mockIntersectionObserver.mockReset()
|
|
19
|
+
vi.clearAllMocks()
|
|
72
20
|
})
|
|
73
21
|
|
|
74
|
-
test('
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
render(
|
|
84
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
85
|
-
<TestComponent {...mockDocument} />
|
|
86
|
-
</ResourceProvider>,
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
// Initially, element is not intersecting
|
|
90
|
-
expect(screen.getByText('Initial Title')).toBeInTheDocument()
|
|
91
|
-
expect(subscribe).not.toHaveBeenCalled()
|
|
92
|
-
|
|
93
|
-
// Simulate element becoming visible
|
|
94
|
-
await act(async () => {
|
|
95
|
-
intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
// After element becomes visible, events subscription should be active
|
|
99
|
-
expect(subscribe).toHaveBeenCalled()
|
|
100
|
-
expect(eventsUnsubscribe).not.toHaveBeenCalled()
|
|
22
|
+
test('transforms projection result to preview format', () => {
|
|
23
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
24
|
+
_id: 'doc1',
|
|
25
|
+
_type: 'exampleType',
|
|
26
|
+
_updatedAt: '2024-01-01',
|
|
27
|
+
titleCandidates: {title: 'Test Title'},
|
|
28
|
+
subtitleCandidates: {description: 'Test Description'},
|
|
29
|
+
media: null,
|
|
30
|
+
}
|
|
101
31
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
32
|
+
vi.mocked(useDocumentProjection).mockReturnValue({
|
|
33
|
+
data: mockProjectionResult,
|
|
34
|
+
isPending: false,
|
|
105
35
|
})
|
|
106
36
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
expect(
|
|
37
|
+
const {result} = renderHook(() => useDocumentPreview(mockDocument))
|
|
38
|
+
const {data, isPending} = result.current
|
|
39
|
+
expect(data.title).toBe('Test Title')
|
|
40
|
+
expect(data.subtitle).toBe('Test Description')
|
|
41
|
+
expect(isPending).toBe(false)
|
|
110
42
|
})
|
|
111
43
|
|
|
112
|
-
test
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
44
|
+
test('handles pending state', () => {
|
|
45
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
46
|
+
_id: 'doc1',
|
|
47
|
+
_type: 'exampleType',
|
|
48
|
+
_updatedAt: '2024-01-01',
|
|
49
|
+
titleCandidates: {title: 'Loading Title'},
|
|
50
|
+
subtitleCandidates: {},
|
|
118
51
|
media: null,
|
|
119
|
-
}
|
|
120
|
-
;(resolvePreview as Mock).mockReturnValueOnce(resolvePromise)
|
|
52
|
+
}
|
|
121
53
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return vi.fn()
|
|
54
|
+
vi.mocked(useDocumentProjection).mockReturnValue({
|
|
55
|
+
data: mockProjectionResult,
|
|
56
|
+
isPending: true,
|
|
126
57
|
})
|
|
127
58
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
59
|
+
function TestComponent() {
|
|
60
|
+
const {data, isPending} = useDocumentPreview(mockDocument)
|
|
61
|
+
return (
|
|
62
|
+
<div>
|
|
63
|
+
<h1>{data.title}</h1>
|
|
64
|
+
{isPending && <div>Pending...</div>}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
133
68
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Simulate element becoming visible
|
|
137
|
-
await act(async () => {
|
|
138
|
-
intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
|
|
139
|
-
await resolvePromise
|
|
140
|
-
getCurrent.mockReturnValue({
|
|
141
|
-
data: {title: 'Resolved Title', subtitle: 'Resolved Subtitle'},
|
|
142
|
-
isPending: false,
|
|
143
|
-
})
|
|
144
|
-
subscriber?.()
|
|
145
|
-
})
|
|
69
|
+
render(<TestComponent />)
|
|
146
70
|
|
|
147
|
-
expect(screen.getByText('
|
|
148
|
-
expect(screen.getByText('
|
|
71
|
+
expect(screen.getByText('Loading Title')).toBeInTheDocument()
|
|
72
|
+
expect(screen.getByText('Pending...')).toBeInTheDocument()
|
|
149
73
|
})
|
|
150
74
|
|
|
151
|
-
test('
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
75
|
+
test('uses fallback title when no candidates exist', () => {
|
|
76
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
77
|
+
_id: 'doc1',
|
|
78
|
+
_type: 'article',
|
|
79
|
+
_updatedAt: '2024-01-01',
|
|
80
|
+
titleCandidates: {},
|
|
81
|
+
subtitleCandidates: {},
|
|
82
|
+
media: null,
|
|
83
|
+
}
|
|
156
84
|
|
|
157
|
-
|
|
158
|
-
data:
|
|
85
|
+
vi.mocked(useDocumentProjection).mockReturnValue({
|
|
86
|
+
data: mockProjectionResult,
|
|
159
87
|
isPending: false,
|
|
160
88
|
})
|
|
161
|
-
subscribe.mockImplementation(() => vi.fn())
|
|
162
89
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
</ResourceProvider>,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
expect(screen.getByText('Fallback Title')).toBeInTheDocument()
|
|
170
|
-
|
|
171
|
-
// Restore IntersectionObserver
|
|
172
|
-
window.IntersectionObserver = originalIntersectionObserver
|
|
90
|
+
const {result} = renderHook(() => useDocumentPreview(mockDocument))
|
|
91
|
+
const {data} = result.current
|
|
92
|
+
expect(data.title).toBe('article: doc1')
|
|
173
93
|
})
|
|
174
94
|
|
|
175
|
-
test('
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
function NoRefComponent(docHandle: DocumentHandle) {
|
|
184
|
-
const {data} = useDocumentPreview(docHandle) // No ref provided
|
|
185
|
-
return (
|
|
186
|
-
<div>
|
|
187
|
-
<h1>{data?.title}</h1>
|
|
188
|
-
<p>{data?.subtitle}</p>
|
|
189
|
-
</div>
|
|
190
|
-
)
|
|
95
|
+
test('passes ref to useDocumentProjection', () => {
|
|
96
|
+
const mockProjectionResult: PreviewQueryResult = {
|
|
97
|
+
_id: 'doc1',
|
|
98
|
+
_type: 'exampleType',
|
|
99
|
+
_updatedAt: '2024-01-01',
|
|
100
|
+
titleCandidates: {title: 'Title'},
|
|
101
|
+
subtitleCandidates: {},
|
|
102
|
+
media: null,
|
|
191
103
|
}
|
|
192
104
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<NoRefComponent {...mockDocument} />
|
|
196
|
-
</ResourceProvider>,
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
// Should subscribe immediately without waiting for intersection
|
|
200
|
-
expect(subscribe).toHaveBeenCalled()
|
|
201
|
-
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test('it subscribes immediately when ref.current is not an HTML element', async () => {
|
|
205
|
-
getCurrent.mockReturnValue({
|
|
206
|
-
data: {title: 'Title', subtitle: 'Subtitle'},
|
|
105
|
+
vi.mocked(useDocumentProjection).mockReturnValue({
|
|
106
|
+
data: mockProjectionResult,
|
|
207
107
|
isPending: false,
|
|
208
108
|
})
|
|
209
|
-
const eventsUnsubscribe = vi.fn()
|
|
210
|
-
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
211
|
-
|
|
212
|
-
function NonHtmlRefComponent(docHandle: DocumentHandle) {
|
|
213
|
-
const ref = useRef({}) // ref.current is not an HTML element
|
|
214
|
-
const {data} = useDocumentPreview({...docHandle, ref})
|
|
215
|
-
return (
|
|
216
|
-
<div>
|
|
217
|
-
<h1>{data?.title}</h1>
|
|
218
|
-
<p>{data?.subtitle}</p>
|
|
219
|
-
</div>
|
|
220
|
-
)
|
|
221
|
-
}
|
|
222
109
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
110
|
+
const ref = {current: null}
|
|
111
|
+
const {result} = renderHook(() => useDocumentPreview({...mockDocument, ref}))
|
|
112
|
+
const {data} = result.current
|
|
113
|
+
expect(data.title).toBe('Title')
|
|
114
|
+
|
|
115
|
+
// Verify useDocumentProjection was called with the ref and preview projection
|
|
116
|
+
expect(useDocumentProjection).toHaveBeenCalledWith(
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
documentId: 'doc1',
|
|
119
|
+
documentType: 'exampleType',
|
|
120
|
+
ref: ref,
|
|
121
|
+
projection: expect.any(String),
|
|
122
|
+
}),
|
|
227
123
|
)
|
|
228
|
-
|
|
229
|
-
// Should subscribe immediately without waiting for intersection
|
|
230
|
-
expect(subscribe).toHaveBeenCalled()
|
|
231
|
-
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
232
124
|
})
|
|
233
125
|
})
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
PREVIEW_PROJECTION,
|
|
3
|
+
type PreviewQueryResult,
|
|
4
|
+
type PreviewValue,
|
|
5
|
+
transformProjectionToPreview,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {useMemo} from 'react'
|
|
4
8
|
|
|
9
|
+
import {type DocumentHandle} from '../../config/handles'
|
|
5
10
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
11
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
12
|
+
import {useDocumentProjection} from '../projection/useDocumentProjection'
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* @public
|
|
@@ -21,7 +28,7 @@ export interface useDocumentPreviewOptions extends DocumentHandle {
|
|
|
21
28
|
* @category Types
|
|
22
29
|
*/
|
|
23
30
|
export interface useDocumentPreviewResults {
|
|
24
|
-
/** The results of inferring the document
|
|
31
|
+
/** The results of inferring the document's preview values */
|
|
25
32
|
data: PreviewValue
|
|
26
33
|
/** True when inferred preview values are being refreshed */
|
|
27
34
|
isPending: boolean
|
|
@@ -31,7 +38,7 @@ export interface useDocumentPreviewResults {
|
|
|
31
38
|
* @public
|
|
32
39
|
*
|
|
33
40
|
* Attempts to infer preview values of a document (specified via a `DocumentHandle`),
|
|
34
|
-
* including the document
|
|
41
|
+
* including the document's `title`, `subtitle`, `media`, and `status`. These values are live and will update in realtime.
|
|
35
42
|
* To reduce unnecessary network requests for resolving the preview values, an optional `ref` can be passed to the hook so that preview
|
|
36
43
|
* resolution will only occur if the `ref` is intersecting the current viewport.
|
|
37
44
|
*
|
|
@@ -40,18 +47,22 @@ export interface useDocumentPreviewResults {
|
|
|
40
47
|
* @remarks
|
|
41
48
|
* Values returned by this hook may not be as expected. It is currently unable to read preview values as defined in your schema;
|
|
42
49
|
* instead, it attempts to infer these preview values by checking against a basic set of potential fields on your document.
|
|
43
|
-
* We are anticipating being able to significantly improve this hook
|
|
50
|
+
* We are anticipating being able to significantly improve this hook's functionality and output in a future release.
|
|
44
51
|
* For now, we recommend using {@link useDocumentProjection} for rendering individual document fields (or projections of those fields).
|
|
45
52
|
*
|
|
53
|
+
* Internally, this hook is implemented as a specialized projection with post-processing logic.
|
|
54
|
+
* It uses a fixed GROQ projection to fetch common preview fields (title, subtitle, media) and
|
|
55
|
+
* transforms the results into the PreviewValue format.
|
|
56
|
+
*
|
|
46
57
|
* @category Documents
|
|
47
58
|
* @param options - The document handle for the document you want to infer preview values for, and an optional ref
|
|
48
59
|
* @returns The inferred values for the given document and a boolean to indicate whether the resolution is pending
|
|
49
60
|
*
|
|
50
61
|
* @example Combining with useDocuments to render a collection of document previews
|
|
51
62
|
* ```
|
|
52
|
-
* // PreviewComponent.
|
|
53
|
-
* export default function PreviewComponent(
|
|
54
|
-
* const { data: { title, subtitle, media }, isPending } = useDocumentPreview(
|
|
63
|
+
* // PreviewComponent.tsx
|
|
64
|
+
* export default function PreviewComponent(docHandle: DocumentHandle) {
|
|
65
|
+
* const { data: { title, subtitle, media }, isPending } = useDocumentPreview(docHandle)
|
|
55
66
|
* return (
|
|
56
67
|
* <article style={{ opacity: isPending ? 0.5 : 1}}>
|
|
57
68
|
* {media?.type === 'image-asset' ? <img src={media.url} alt='' /> : ''}
|
|
@@ -61,16 +72,16 @@ export interface useDocumentPreviewResults {
|
|
|
61
72
|
* )
|
|
62
73
|
* }
|
|
63
74
|
*
|
|
64
|
-
* // DocumentList.
|
|
65
|
-
* const { data } = useDocuments({
|
|
75
|
+
* // DocumentList.tsx
|
|
76
|
+
* const { data } = useDocuments({ documentType: 'movie' })
|
|
66
77
|
* return (
|
|
67
78
|
* <div>
|
|
68
79
|
* <h1>Movies</h1>
|
|
69
80
|
* <ul>
|
|
70
81
|
* {data.map(movie => (
|
|
71
|
-
* <li key={movie.
|
|
82
|
+
* <li key={movie.documentId}>
|
|
72
83
|
* <Suspense fallback='Loading…'>
|
|
73
|
-
* <PreviewComponent
|
|
84
|
+
* <PreviewComponent {...movie} />
|
|
74
85
|
* </Suspense>
|
|
75
86
|
* </li>
|
|
76
87
|
* ))}
|
|
@@ -83,57 +94,26 @@ export function useDocumentPreview({
|
|
|
83
94
|
ref,
|
|
84
95
|
...docHandle
|
|
85
96
|
}: useDocumentPreviewOptions): useDocumentPreviewResults {
|
|
86
|
-
const instance = useSanityInstance(
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
// Create subscribe function for useSyncExternalStore
|
|
90
|
-
const subscribe = useCallback(
|
|
91
|
-
(onStoreChanged: () => void) => {
|
|
92
|
-
const subscription = new Observable<boolean>((observer) => {
|
|
93
|
-
// For environments that don't have an intersection observer (e.g. server-side),
|
|
94
|
-
// we pass true to always subscribe since we can't detect visibility
|
|
95
|
-
if (typeof IntersectionObserver === 'undefined' || typeof HTMLElement === 'undefined') {
|
|
96
|
-
observer.next(true)
|
|
97
|
-
return
|
|
98
|
-
}
|
|
97
|
+
const instance = useSanityInstance()
|
|
98
|
+
const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} else {
|
|
107
|
-
// If no ref is provided or ref.current isn't an HTML element,
|
|
108
|
-
// pass true to always subscribe since we can't track visibility
|
|
109
|
-
observer.next(true)
|
|
110
|
-
}
|
|
111
|
-
return () => intersectionObserver.disconnect()
|
|
112
|
-
})
|
|
113
|
-
.pipe(
|
|
114
|
-
startWith(false),
|
|
115
|
-
distinctUntilChanged(),
|
|
116
|
-
switchMap((isVisible) =>
|
|
117
|
-
isVisible
|
|
118
|
-
? new Observable<void>((obs) => {
|
|
119
|
-
return stateSource.subscribe(() => obs.next())
|
|
120
|
-
})
|
|
121
|
-
: EMPTY,
|
|
122
|
-
),
|
|
123
|
-
)
|
|
124
|
-
.subscribe({next: onStoreChanged})
|
|
100
|
+
// Use the projection hook with the fixed preview projection
|
|
101
|
+
const projectionResult = useDocumentProjection<PreviewQueryResult>({
|
|
102
|
+
...normalizedDocHandle,
|
|
103
|
+
projection: PREVIEW_PROJECTION,
|
|
104
|
+
ref,
|
|
105
|
+
})
|
|
125
106
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
107
|
+
// Contract: useDocumentProjection suspends while data is null, so data is always available here.
|
|
108
|
+
// Keep this non-null assumption aligned with useDocumentPreviewResults.data.
|
|
109
|
+
const previewValue = useMemo(
|
|
110
|
+
() =>
|
|
111
|
+
transformProjectionToPreview(instance, normalizedDocHandle.resource, projectionResult.data),
|
|
112
|
+
[projectionResult.data, instance, normalizedDocHandle.resource],
|
|
129
113
|
)
|
|
130
114
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return currentState as useDocumentPreviewResults
|
|
136
|
-
}, [docHandle, instance, stateSource])
|
|
137
|
-
|
|
138
|
-
return useSyncExternalStore(subscribe, getSnapshot)
|
|
115
|
+
return {
|
|
116
|
+
data: previewValue,
|
|
117
|
+
isPending: projectionResult.isPending,
|
|
118
|
+
}
|
|
139
119
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {type DocumentHandle, getProjectionState, resolveProjection} from '@sanity/sdk'
|
|
2
|
-
import {act, render, screen} from '@testing-library/react'
|
|
3
2
|
import {useRef} from 'react'
|
|
4
3
|
import {type Mock} from 'vitest'
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import {act, render, screen} from '../../../test/test-utils'
|
|
7
6
|
import {useDocumentProjection} from './useDocumentProjection'
|
|
8
7
|
|
|
9
8
|
// Mock IntersectionObserver
|
|
@@ -40,6 +39,7 @@ vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
|
40
39
|
const mockDocument: DocumentHandle = {
|
|
41
40
|
documentId: 'doc1',
|
|
42
41
|
documentType: 'exampleType',
|
|
42
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
interface ProjectionResult {
|
|
@@ -85,11 +85,7 @@ describe('useDocumentProjection', () => {
|
|
|
85
85
|
const eventsUnsubscribe = vi.fn()
|
|
86
86
|
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
87
87
|
|
|
88
|
-
render(
|
|
89
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
90
|
-
<TestComponent document={mockDocument} projection="{name, description}" />
|
|
91
|
-
</ResourceProvider>,
|
|
92
|
-
)
|
|
88
|
+
render(<TestComponent document={mockDocument} projection="{name, description}" />)
|
|
93
89
|
|
|
94
90
|
// Initially, element is not intersecting
|
|
95
91
|
expect(screen.getByText('Initial Title')).toBeInTheDocument()
|
|
@@ -135,11 +131,7 @@ describe('useDocumentProjection', () => {
|
|
|
135
131
|
// Setup subscription that does nothing (we'll manually trigger updates)
|
|
136
132
|
subscribe.mockReturnValue(() => {})
|
|
137
133
|
|
|
138
|
-
render(
|
|
139
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
140
|
-
<TestComponent document={mockDocument} projection="{title, description}" />
|
|
141
|
-
</ResourceProvider>,
|
|
142
|
-
)
|
|
134
|
+
render(<TestComponent document={mockDocument} projection="{title, description}" />)
|
|
143
135
|
|
|
144
136
|
await act(async () => {
|
|
145
137
|
intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
|
|
@@ -162,11 +154,7 @@ describe('useDocumentProjection', () => {
|
|
|
162
154
|
})
|
|
163
155
|
subscribe.mockImplementation(() => vi.fn())
|
|
164
156
|
|
|
165
|
-
render(
|
|
166
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
167
|
-
<TestComponent document={mockDocument} projection="{title, description}" />
|
|
168
|
-
</ResourceProvider>,
|
|
169
|
-
)
|
|
157
|
+
render(<TestComponent document={mockDocument} projection="{title, description}" />)
|
|
170
158
|
|
|
171
159
|
expect(screen.getByText('Fallback Title')).toBeInTheDocument()
|
|
172
160
|
|
|
@@ -182,11 +170,7 @@ describe('useDocumentProjection', () => {
|
|
|
182
170
|
const eventsUnsubscribe = vi.fn()
|
|
183
171
|
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
184
172
|
|
|
185
|
-
const {rerender} = render(
|
|
186
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
187
|
-
<TestComponent document={mockDocument} projection="{title}" />
|
|
188
|
-
</ResourceProvider>,
|
|
189
|
-
)
|
|
173
|
+
const {rerender} = render(<TestComponent document={mockDocument} projection="{title}" />)
|
|
190
174
|
|
|
191
175
|
// Change projection
|
|
192
176
|
getCurrent.mockReturnValue({
|
|
@@ -194,11 +178,7 @@ describe('useDocumentProjection', () => {
|
|
|
194
178
|
isPending: false,
|
|
195
179
|
})
|
|
196
180
|
|
|
197
|
-
rerender(
|
|
198
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
199
|
-
<TestComponent document={mockDocument} projection="{title, description}" />
|
|
200
|
-
</ResourceProvider>,
|
|
201
|
-
)
|
|
181
|
+
rerender(<TestComponent document={mockDocument} projection="{title, description}" />)
|
|
202
182
|
|
|
203
183
|
expect(screen.getByText('Updated Title')).toBeInTheDocument()
|
|
204
184
|
expect(screen.getByText('Added Description')).toBeInTheDocument()
|
|
@@ -222,11 +202,7 @@ describe('useDocumentProjection', () => {
|
|
|
222
202
|
)
|
|
223
203
|
}
|
|
224
204
|
|
|
225
|
-
render(
|
|
226
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
227
|
-
<NoRefComponent {...mockDocument} projection="{title, description}" />
|
|
228
|
-
</ResourceProvider>,
|
|
229
|
-
)
|
|
205
|
+
render(<NoRefComponent {...mockDocument} projection="{title, description}" />)
|
|
230
206
|
|
|
231
207
|
// Should subscribe immediately without waiting for intersection
|
|
232
208
|
expect(subscribe).toHaveBeenCalled()
|
|
@@ -255,11 +231,7 @@ describe('useDocumentProjection', () => {
|
|
|
255
231
|
)
|
|
256
232
|
}
|
|
257
233
|
|
|
258
|
-
render(
|
|
259
|
-
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
260
|
-
<NonHtmlRefComponent {...mockDocument} projection="{title, description}" />
|
|
261
|
-
</ResourceProvider>,
|
|
262
|
-
)
|
|
234
|
+
render(<NonHtmlRefComponent {...mockDocument} projection="{title, description}" />)
|
|
263
235
|
|
|
264
236
|
// Should subscribe immediately without waiting for intersection
|
|
265
237
|
expect(subscribe).toHaveBeenCalled()
|