@sanity/sdk-react 0.0.0-alpha.21 → 0.0.0-alpha.23
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 +502 -3460
- package/dist/index.js +400 -465
- package/dist/index.js.map +1 -1
- package/package.json +17 -15
- package/src/_exports/index.ts +4 -5
- package/src/components/SDKProvider.test.tsx +78 -54
- package/src/components/SDKProvider.tsx +31 -26
- package/src/components/SanityApp.test.tsx +121 -15
- package/src/components/SanityApp.tsx +26 -15
- package/src/components/auth/AuthBoundary.test.tsx +32 -14
- package/src/components/auth/AuthBoundary.tsx +53 -23
- package/src/components/auth/LoginCallback.test.tsx +19 -6
- package/src/components/auth/LoginCallback.tsx +2 -11
- package/src/components/auth/LoginError.test.tsx +12 -4
- package/src/components/auth/LoginError.tsx +13 -21
- package/src/components/auth/LoginFooter.test.tsx +7 -3
- package/src/context/ResourceProvider.test.tsx +157 -0
- package/src/context/ResourceProvider.tsx +111 -0
- package/src/context/SanityInstanceContext.ts +1 -1
- package/src/hooks/auth/useLoginUrl.tsx +14 -0
- package/src/hooks/client/useClient.ts +2 -1
- package/src/hooks/comlink/useManageFavorite.test.ts +16 -8
- package/src/hooks/comlink/useManageFavorite.ts +37 -13
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +8 -4
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +10 -8
- package/src/hooks/context/useSanityInstance.test.tsx +157 -15
- package/src/hooks/context/useSanityInstance.ts +66 -26
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +13 -31
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +12 -15
- package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.test.tsx → useStudioWorkspacesByProjectIdDataset.test.tsx} +13 -13
- package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.ts → useStudioWorkspacesByProjectIdDataset.ts} +10 -9
- package/src/hooks/datasets/useDatasets.ts +15 -4
- package/src/hooks/document/useApplyDocumentActions.test.ts +4 -9
- package/src/hooks/document/useApplyDocumentActions.ts +6 -31
- package/src/hooks/document/useDocument.test.ts +2 -2
- package/src/hooks/document/useDocument.ts +40 -19
- package/src/hooks/document/useDocumentEvent.test.ts +2 -3
- package/src/hooks/document/useDocumentEvent.ts +7 -11
- package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
- package/src/hooks/document/useDocumentPermissions.ts +31 -23
- package/src/hooks/document/useDocumentSyncStatus.ts +5 -4
- package/src/hooks/document/useEditDocument.test.ts +2 -3
- package/src/hooks/document/useEditDocument.ts +43 -29
- package/src/hooks/documents/useDocuments.test.tsx +30 -3
- package/src/hooks/documents/useDocuments.ts +20 -7
- package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
- package/src/hooks/helpers/createCallbackHook.tsx +2 -3
- package/src/hooks/helpers/createStateSourceHook.test.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +5 -8
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +43 -18
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +36 -50
- package/src/hooks/preview/usePreview.test.tsx +66 -7
- package/src/hooks/preview/usePreview.tsx +17 -12
- package/src/hooks/projection/useProjection.test.tsx +68 -3
- package/src/hooks/projection/useProjection.ts +21 -24
- package/src/hooks/projects/useProject.ts +7 -4
- package/src/hooks/query/useQuery.ts +32 -14
- package/src/hooks/users/useUsers.test.tsx +330 -0
- package/src/hooks/users/useUsers.ts +65 -52
- package/src/components/Login/LoginLinks.test.tsx +0 -90
- package/src/components/Login/LoginLinks.tsx +0 -58
- package/src/components/auth/Login.test.tsx +0 -27
- package/src/components/auth/Login.tsx +0 -39
- package/src/components/auth/LoginLayout.test.tsx +0 -19
- package/src/components/auth/LoginLayout.tsx +0 -69
- package/src/components/auth/authTestHelpers.tsx +0 -11
- package/src/context/SanityProvider.test.tsx +0 -25
- package/src/context/SanityProvider.tsx +0 -50
- package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
- package/src/hooks/auth/useLoginUrls.tsx +0 -52
- package/src/hooks/users/useUsers.test.ts +0 -163
|
@@ -55,7 +55,7 @@ describe('createCallbackHook', () => {
|
|
|
55
55
|
vi.mocked(useSanityInstance).mockReturnValueOnce(mockInstance1)
|
|
56
56
|
|
|
57
57
|
// Create a test callback
|
|
58
|
-
const testCallback = (instance: SanityInstance) => instance.
|
|
58
|
+
const testCallback = (instance: SanityInstance) => instance.config.projectId
|
|
59
59
|
|
|
60
60
|
// Create and render our hook
|
|
61
61
|
const useTestHook = createCallbackHook(testCallback)
|
|
@@ -87,7 +87,7 @@ describe('createCallbackHook', () => {
|
|
|
87
87
|
method: string,
|
|
88
88
|
data: object,
|
|
89
89
|
) => ({
|
|
90
|
-
url: `${instance.
|
|
90
|
+
url: `${instance.config.projectId}${path}`,
|
|
91
91
|
method,
|
|
92
92
|
data,
|
|
93
93
|
})
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {useCallback} from 'react'
|
|
3
3
|
|
|
4
4
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
5
|
|
|
6
6
|
export function createCallbackHook<TParams extends unknown[], TReturn>(
|
|
7
7
|
callback: (instance: SanityInstance, ...params: TParams) => TReturn,
|
|
8
|
-
resourceId?: ResourceId,
|
|
9
8
|
): () => (...params: TParams) => TReturn {
|
|
10
9
|
function useHook() {
|
|
11
|
-
const instance = useSanityInstance(
|
|
10
|
+
const instance = useSanityInstance()
|
|
12
11
|
return useCallback((...params: TParams) => callback(instance, ...params), [instance])
|
|
13
12
|
}
|
|
14
13
|
|
|
@@ -72,7 +72,7 @@ describe('createStateSourceHook', () => {
|
|
|
72
72
|
|
|
73
73
|
const stateSourceFactory = vi.fn((instance: SanityInstance) => ({
|
|
74
74
|
subscribe: vi.fn(),
|
|
75
|
-
getCurrent: () => instance.
|
|
75
|
+
getCurrent: () => instance.config.projectId,
|
|
76
76
|
observable: throwError(() => new Error('unexpected usage of observable')),
|
|
77
77
|
}))
|
|
78
78
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type SanityConfig, type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
2
2
|
import {useSyncExternalStore} from 'react'
|
|
3
3
|
|
|
4
4
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -12,22 +12,19 @@ interface CreateStateSourceHookOptions<TParams extends unknown[], TState> {
|
|
|
12
12
|
getState: StateSourceFactory<TParams, TState>
|
|
13
13
|
shouldSuspend?: (instance: SanityInstance, ...params: TParams) => boolean
|
|
14
14
|
suspender?: (instance: SanityInstance, ...params: TParams) => Promise<unknown>
|
|
15
|
-
|
|
15
|
+
getConfig?: (...params: TParams) => SanityConfig | undefined
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function createStateSourceHook<TParams extends unknown[], TState>(
|
|
19
19
|
options: StateSourceFactory<TParams, TState> | CreateStateSourceHookOptions<TParams, TState>,
|
|
20
20
|
): (...params: TParams) => TState {
|
|
21
21
|
const getState = typeof options === 'function' ? options : options.getState
|
|
22
|
-
const
|
|
22
|
+
const getConfig = 'getConfig' in options ? options.getConfig : undefined
|
|
23
23
|
const suspense = 'shouldSuspend' in options && 'suspender' in options ? options : undefined
|
|
24
24
|
|
|
25
25
|
function useHook(...params: TParams) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
resourceId = getResourceId(...params)
|
|
29
|
-
}
|
|
30
|
-
const instance = useSanityInstance(resourceId)
|
|
26
|
+
const instance = useSanityInstance(getConfig?.(...params))
|
|
27
|
+
|
|
31
28
|
if (suspense?.suspender && suspense?.shouldSuspend?.(instance, ...params)) {
|
|
32
29
|
throw suspense.suspender(instance, ...params)
|
|
33
30
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {act, renderHook} from '@testing-library/react'
|
|
2
2
|
import {describe, vi} from 'vitest'
|
|
3
3
|
|
|
4
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
4
5
|
import {evaluateSync, parse} from '../_synchronous-groq-js.mjs'
|
|
5
6
|
import {useQuery} from '../query/useQuery'
|
|
6
7
|
import {usePaginatedDocuments} from './usePaginatedDocuments'
|
|
@@ -8,6 +9,12 @@ import {usePaginatedDocuments} from './usePaginatedDocuments'
|
|
|
8
9
|
vi.mock('../query/useQuery')
|
|
9
10
|
|
|
10
11
|
describe('usePaginatedDocuments', () => {
|
|
12
|
+
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
13
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
14
|
+
{children}
|
|
15
|
+
</ResourceProvider>
|
|
16
|
+
)
|
|
17
|
+
|
|
11
18
|
beforeEach(() => {
|
|
12
19
|
const dataset = [
|
|
13
20
|
{
|
|
@@ -76,42 +83,46 @@ describe('usePaginatedDocuments', () => {
|
|
|
76
83
|
|
|
77
84
|
it('should respect custom page size', () => {
|
|
78
85
|
const customPageSize = 2
|
|
79
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize: customPageSize}))
|
|
86
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize: customPageSize}), {wrapper})
|
|
80
87
|
|
|
81
88
|
expect(result.current.pageSize).toBe(customPageSize)
|
|
82
89
|
expect(result.current.data.length).toBeLessThanOrEqual(customPageSize)
|
|
83
90
|
})
|
|
84
91
|
|
|
85
92
|
it('should filter by document type', () => {
|
|
86
|
-
const {result} = renderHook(() => usePaginatedDocuments({filter: '_type == "movie"'})
|
|
93
|
+
const {result} = renderHook(() => usePaginatedDocuments({filter: '_type == "movie"'}), {
|
|
94
|
+
wrapper,
|
|
95
|
+
})
|
|
87
96
|
|
|
88
|
-
expect(result.current.data.every((doc) => doc.
|
|
97
|
+
expect(result.current.data.every((doc) => doc.documentType === 'movie')).toBe(true)
|
|
89
98
|
expect(result.current.count).toBe(5) // 5 movies in the dataset
|
|
90
99
|
})
|
|
91
100
|
|
|
92
101
|
// groq-js doesn't support search filters yet
|
|
93
102
|
it.skip('should apply search filter', () => {
|
|
94
|
-
const {result} = renderHook(() => usePaginatedDocuments({search: 'inter'}))
|
|
103
|
+
const {result} = renderHook(() => usePaginatedDocuments({search: 'inter'}), {wrapper})
|
|
95
104
|
|
|
96
105
|
// Should match "Interstellar"
|
|
97
|
-
expect(result.current.data.some((doc) => doc.
|
|
106
|
+
expect(result.current.data.some((doc) => doc.documentId === 'movie3')).toBe(true)
|
|
98
107
|
})
|
|
99
108
|
|
|
100
109
|
it('should apply ordering', () => {
|
|
101
|
-
const {result} = renderHook(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
const {result} = renderHook(
|
|
111
|
+
() =>
|
|
112
|
+
usePaginatedDocuments({
|
|
113
|
+
filter: '_type == "movie"',
|
|
114
|
+
orderings: [{field: 'releaseYear', direction: 'desc'}],
|
|
115
|
+
}),
|
|
116
|
+
{wrapper},
|
|
106
117
|
)
|
|
107
118
|
|
|
108
119
|
// First item should be the most recent movie (Interstellar, 2014)
|
|
109
|
-
expect(result.current.data[0].
|
|
120
|
+
expect(result.current.data[0].documentId).toBe('movie3')
|
|
110
121
|
})
|
|
111
122
|
|
|
112
123
|
it('should calculate pagination values correctly', () => {
|
|
113
124
|
const pageSize = 2
|
|
114
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
125
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
|
|
115
126
|
|
|
116
127
|
expect(result.current.currentPage).toBe(1)
|
|
117
128
|
expect(result.current.totalPages).toBe(3) // 6 items with page size 2
|
|
@@ -122,7 +133,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
122
133
|
|
|
123
134
|
it('should navigate to next page', () => {
|
|
124
135
|
const pageSize = 2
|
|
125
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
136
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
|
|
126
137
|
|
|
127
138
|
expect(result.current.currentPage).toBe(1)
|
|
128
139
|
expect(result.current.data.length).toBe(pageSize)
|
|
@@ -138,7 +149,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
138
149
|
|
|
139
150
|
it('should navigate to previous page', () => {
|
|
140
151
|
const pageSize = 2
|
|
141
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
152
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
|
|
142
153
|
|
|
143
154
|
// Go to page 2 first
|
|
144
155
|
act(() => {
|
|
@@ -158,7 +169,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
158
169
|
|
|
159
170
|
it('should navigate to first page', () => {
|
|
160
171
|
const pageSize = 2
|
|
161
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
172
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
|
|
162
173
|
|
|
163
174
|
// Go to last page first
|
|
164
175
|
act(() => {
|
|
@@ -178,7 +189,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
178
189
|
|
|
179
190
|
it('should navigate to last page', () => {
|
|
180
191
|
const pageSize = 2
|
|
181
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
192
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
|
|
182
193
|
|
|
183
194
|
act(() => {
|
|
184
195
|
result.current.lastPage()
|
|
@@ -190,7 +201,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
190
201
|
|
|
191
202
|
it('should navigate to specific page', () => {
|
|
192
203
|
const pageSize = 2
|
|
193
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
204
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
|
|
194
205
|
|
|
195
206
|
act(() => {
|
|
196
207
|
result.current.goToPage(2) // Go to page 2
|
|
@@ -215,7 +226,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
215
226
|
|
|
216
227
|
it('should set page availability flags correctly', () => {
|
|
217
228
|
const pageSize = 2
|
|
218
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
229
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
|
|
219
230
|
// On first page
|
|
220
231
|
expect(result.current.hasFirstPage).toBe(false)
|
|
221
232
|
expect(result.current.hasPreviousPage).toBe(false)
|
|
@@ -243,6 +254,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
243
254
|
it('should reset current page when filter changes', () => {
|
|
244
255
|
const {result, rerender} = renderHook((props) => usePaginatedDocuments(props), {
|
|
245
256
|
initialProps: {pageSize: 2, filter: ''},
|
|
257
|
+
wrapper,
|
|
246
258
|
})
|
|
247
259
|
// Initially, current page should be 1
|
|
248
260
|
expect(result.current.currentPage).toBe(1)
|
|
@@ -256,4 +268,17 @@ describe('usePaginatedDocuments', () => {
|
|
|
256
268
|
expect(result.current.currentPage).toBe(1)
|
|
257
269
|
expect(result.current.startIndex).toBe(0)
|
|
258
270
|
})
|
|
271
|
+
|
|
272
|
+
it('should add projectId and dataset to document handles', () => {
|
|
273
|
+
const {result} = renderHook(() => usePaginatedDocuments({}), {wrapper})
|
|
274
|
+
|
|
275
|
+
// Check that the first document handle has the projectId and dataset
|
|
276
|
+
expect(result.current.data[0].projectId).toBe('p')
|
|
277
|
+
expect(result.current.data[0].dataset).toBe('d')
|
|
278
|
+
|
|
279
|
+
// Verify all document handles have these properties
|
|
280
|
+
expect(result.current.data.every((doc) => doc.projectId === 'p' && doc.dataset === 'd')).toBe(
|
|
281
|
+
true,
|
|
282
|
+
)
|
|
283
|
+
})
|
|
259
284
|
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {type DocumentHandle, type QueryOptions} from '@sanity/sdk'
|
|
2
2
|
import {type SortOrderingItem} from '@sanity/types'
|
|
3
|
+
import {pick} from 'lodash-es'
|
|
3
4
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
4
5
|
|
|
6
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
7
|
import {useQuery} from '../query/useQuery'
|
|
6
8
|
|
|
7
9
|
const DEFAULT_PERSPECTIVE = 'drafts'
|
|
@@ -125,7 +127,13 @@ export interface PaginatedDocumentsResponse {
|
|
|
125
127
|
* @category Documents
|
|
126
128
|
* @param options - Configuration options for the paginated list
|
|
127
129
|
* @returns An object containing the current page of document handles, the loading and pagination state, and navigation functions
|
|
128
|
-
*
|
|
130
|
+
*
|
|
131
|
+
* @remarks
|
|
132
|
+
* - The returned document handles include projectId and dataset information from the current Sanity instance
|
|
133
|
+
* - This makes them ready to use with document operations and other document hooks
|
|
134
|
+
* - The hook automatically uses the correct Sanity instance based on the projectId and dataset in the options
|
|
135
|
+
*
|
|
136
|
+
* @example Basic usage
|
|
129
137
|
* ```tsx
|
|
130
138
|
* const {
|
|
131
139
|
* data,
|
|
@@ -147,14 +155,12 @@ export interface PaginatedDocumentsResponse {
|
|
|
147
155
|
* <>
|
|
148
156
|
* <table>
|
|
149
157
|
* {data.map(doc => (
|
|
150
|
-
* <MyTableRowComponent key={doc.
|
|
158
|
+
* <MyTableRowComponent key={doc.documentId} doc={doc} />
|
|
151
159
|
* ))}
|
|
152
160
|
* </table>
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* {hasNextPage && <button onClick={nextPage}>Next</button>}
|
|
157
|
-
* </>
|
|
161
|
+
* {hasPreviousPage && <button onClick={previousPage}>Previous</button>}
|
|
162
|
+
* {currentPage} / {totalPages}
|
|
163
|
+
* {hasNextPage && <button onClick={nextPage}>Next</button>}
|
|
158
164
|
* </>
|
|
159
165
|
* )
|
|
160
166
|
* ```
|
|
@@ -167,7 +173,8 @@ export function usePaginatedDocuments({
|
|
|
167
173
|
orderings,
|
|
168
174
|
search,
|
|
169
175
|
...options
|
|
170
|
-
}: PaginatedDocumentsOptions
|
|
176
|
+
}: PaginatedDocumentsOptions): PaginatedDocumentsResponse {
|
|
177
|
+
const instance = useSanityInstance(options)
|
|
171
178
|
const [pageIndex, setPageIndex] = useState(0)
|
|
172
179
|
const key = JSON.stringify({filter, search, params, orderings, pageSize})
|
|
173
180
|
// Reset the pageIndex to 0 whenever any query parameters (filter, search,
|
|
@@ -207,7 +214,7 @@ export function usePaginatedDocuments({
|
|
|
207
214
|
.join(',')})`
|
|
208
215
|
: ''
|
|
209
216
|
|
|
210
|
-
const dataQuery = `*${filterClause}${orderClause}[${startIndex}...${endIndex}]{_id,_type}`
|
|
217
|
+
const dataQuery = `*${filterClause}${orderClause}[${startIndex}...${endIndex}]{"documentId":_id,"documentType":_type,...$__dataset}`
|
|
211
218
|
const countQuery = `count(*${filterClause})`
|
|
212
219
|
|
|
213
220
|
const {
|
|
@@ -218,7 +225,7 @@ export function usePaginatedDocuments({
|
|
|
218
225
|
{
|
|
219
226
|
...options,
|
|
220
227
|
perspective,
|
|
221
|
-
params,
|
|
228
|
+
params: {...params, __dataset: pick(instance.config, 'projectId', 'dataset')},
|
|
222
229
|
},
|
|
223
230
|
)
|
|
224
231
|
|
|
@@ -247,44 +254,23 @@ export function usePaginatedDocuments({
|
|
|
247
254
|
const hasNextPage = pageIndex < totalPages - 1
|
|
248
255
|
const hasLastPage = pageIndex < totalPages - 1
|
|
249
256
|
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}),
|
|
270
|
-
[
|
|
271
|
-
data,
|
|
272
|
-
isPending,
|
|
273
|
-
pageSize,
|
|
274
|
-
currentPage,
|
|
275
|
-
totalPages,
|
|
276
|
-
startIndex,
|
|
277
|
-
endIndex,
|
|
278
|
-
count,
|
|
279
|
-
firstPage,
|
|
280
|
-
hasFirstPage,
|
|
281
|
-
previousPage,
|
|
282
|
-
hasPreviousPage,
|
|
283
|
-
nextPage,
|
|
284
|
-
hasNextPage,
|
|
285
|
-
lastPage,
|
|
286
|
-
hasLastPage,
|
|
287
|
-
goToPage,
|
|
288
|
-
],
|
|
289
|
-
)
|
|
257
|
+
return {
|
|
258
|
+
data,
|
|
259
|
+
isPending,
|
|
260
|
+
pageSize,
|
|
261
|
+
currentPage,
|
|
262
|
+
totalPages,
|
|
263
|
+
startIndex,
|
|
264
|
+
endIndex,
|
|
265
|
+
count,
|
|
266
|
+
firstPage,
|
|
267
|
+
hasFirstPage,
|
|
268
|
+
previousPage,
|
|
269
|
+
hasPreviousPage,
|
|
270
|
+
nextPage,
|
|
271
|
+
hasNextPage,
|
|
272
|
+
lastPage,
|
|
273
|
+
hasLastPage,
|
|
274
|
+
goToPage,
|
|
275
|
+
}
|
|
290
276
|
}
|
|
@@ -39,13 +39,13 @@ vi.mock('../context/useSanityInstance', () => ({
|
|
|
39
39
|
}))
|
|
40
40
|
|
|
41
41
|
const mockDocument: DocumentHandle = {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
documentId: 'doc1',
|
|
43
|
+
documentType: 'exampleType',
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function TestComponent(
|
|
46
|
+
function TestComponent(docHandle: DocumentHandle) {
|
|
47
47
|
const ref = useRef(null)
|
|
48
|
-
const {data, isPending} = usePreview({
|
|
48
|
+
const {data, isPending} = usePreview({...docHandle, ref})
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
51
|
<div ref={ref}>
|
|
@@ -83,7 +83,7 @@ describe('usePreview', () => {
|
|
|
83
83
|
|
|
84
84
|
render(
|
|
85
85
|
<Suspense fallback={<div>Loading...</div>}>
|
|
86
|
-
<TestComponent
|
|
86
|
+
<TestComponent {...mockDocument} />
|
|
87
87
|
</Suspense>,
|
|
88
88
|
)
|
|
89
89
|
|
|
@@ -128,7 +128,7 @@ describe('usePreview', () => {
|
|
|
128
128
|
|
|
129
129
|
render(
|
|
130
130
|
<Suspense fallback={<div>Loading...</div>}>
|
|
131
|
-
<TestComponent
|
|
131
|
+
<TestComponent {...mockDocument} />
|
|
132
132
|
</Suspense>,
|
|
133
133
|
)
|
|
134
134
|
|
|
@@ -163,7 +163,7 @@ describe('usePreview', () => {
|
|
|
163
163
|
|
|
164
164
|
render(
|
|
165
165
|
<Suspense fallback={<div>Loading...</div>}>
|
|
166
|
-
<TestComponent
|
|
166
|
+
<TestComponent {...mockDocument} />
|
|
167
167
|
</Suspense>,
|
|
168
168
|
)
|
|
169
169
|
|
|
@@ -172,4 +172,63 @@ describe('usePreview', () => {
|
|
|
172
172
|
// Restore IntersectionObserver
|
|
173
173
|
window.IntersectionObserver = originalIntersectionObserver
|
|
174
174
|
})
|
|
175
|
+
|
|
176
|
+
test('it subscribes immediately when no ref is provided', async () => {
|
|
177
|
+
getCurrent.mockReturnValue({
|
|
178
|
+
data: {title: 'Title', subtitle: 'Subtitle'},
|
|
179
|
+
isPending: false,
|
|
180
|
+
})
|
|
181
|
+
const eventsUnsubscribe = vi.fn()
|
|
182
|
+
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
183
|
+
|
|
184
|
+
function NoRefComponent(docHandle: DocumentHandle) {
|
|
185
|
+
const {data} = usePreview(docHandle) // No ref provided
|
|
186
|
+
return (
|
|
187
|
+
<div>
|
|
188
|
+
<h1>{data?.title}</h1>
|
|
189
|
+
<p>{data?.subtitle}</p>
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
render(
|
|
195
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
196
|
+
<NoRefComponent {...mockDocument} />
|
|
197
|
+
</Suspense>,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// Should subscribe immediately without waiting for intersection
|
|
201
|
+
expect(subscribe).toHaveBeenCalled()
|
|
202
|
+
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('it subscribes immediately when ref.current is not an HTML element', async () => {
|
|
206
|
+
getCurrent.mockReturnValue({
|
|
207
|
+
data: {title: 'Title', subtitle: 'Subtitle'},
|
|
208
|
+
isPending: false,
|
|
209
|
+
})
|
|
210
|
+
const eventsUnsubscribe = vi.fn()
|
|
211
|
+
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
212
|
+
|
|
213
|
+
function NonHtmlRefComponent(docHandle: DocumentHandle) {
|
|
214
|
+
const ref = useRef({}) // ref.current is not an HTML element
|
|
215
|
+
const {data} = usePreview({...docHandle, ref})
|
|
216
|
+
return (
|
|
217
|
+
<div>
|
|
218
|
+
<h1>{data?.title}</h1>
|
|
219
|
+
<p>{data?.subtitle}</p>
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
render(
|
|
225
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
226
|
+
<NonHtmlRefComponent {...mockDocument} />
|
|
227
|
+
</Suspense>,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
// Should subscribe immediately without waiting for intersection
|
|
231
|
+
expect(subscribe).toHaveBeenCalled()
|
|
232
|
+
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
233
|
+
})
|
|
175
234
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {type DocumentHandle, getPreviewState, type PreviewValue, resolvePreview} from '@sanity/sdk'
|
|
2
|
-
import {useCallback,
|
|
2
|
+
import {useCallback, useSyncExternalStore} from 'react'
|
|
3
3
|
import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
|
|
4
4
|
|
|
5
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -8,8 +8,11 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
8
8
|
* @beta
|
|
9
9
|
* @category Types
|
|
10
10
|
*/
|
|
11
|
-
export interface UsePreviewOptions {
|
|
12
|
-
|
|
11
|
+
export interface UsePreviewOptions extends DocumentHandle {
|
|
12
|
+
/**
|
|
13
|
+
* Optional ref object to track visibility. When provided, preview resolution
|
|
14
|
+
* only occurs when the referenced element is visible in the viewport.
|
|
15
|
+
*/
|
|
13
16
|
ref?: React.RefObject<unknown>
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -68,20 +71,18 @@ export interface UsePreviewResults {
|
|
|
68
71
|
* )
|
|
69
72
|
* ```
|
|
70
73
|
*/
|
|
71
|
-
export function usePreview({
|
|
74
|
+
export function usePreview({ref, ...docHandle}: UsePreviewOptions): UsePreviewResults {
|
|
72
75
|
const instance = useSanityInstance()
|
|
73
|
-
|
|
74
|
-
const stateSource = useMemo(
|
|
75
|
-
() => getPreviewState(instance, {document: {_id, _type}}),
|
|
76
|
-
[instance, _id, _type],
|
|
77
|
-
)
|
|
76
|
+
const stateSource = getPreviewState(instance, docHandle)
|
|
78
77
|
|
|
79
78
|
// Create subscribe function for useSyncExternalStore
|
|
80
79
|
const subscribe = useCallback(
|
|
81
80
|
(onStoreChanged: () => void) => {
|
|
82
81
|
const subscription = new Observable<boolean>((observer) => {
|
|
83
|
-
//
|
|
82
|
+
// For environments that don't have an intersection observer (e.g. server-side),
|
|
83
|
+
// we pass true to always subscribe since we can't detect visibility
|
|
84
84
|
if (typeof IntersectionObserver === 'undefined' || typeof HTMLElement === 'undefined') {
|
|
85
|
+
observer.next(true)
|
|
85
86
|
return
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -91,6 +92,10 @@ export function usePreview({document: {_id, _type}, ref}: UsePreviewOptions): Us
|
|
|
91
92
|
)
|
|
92
93
|
if (ref?.current && ref.current instanceof HTMLElement) {
|
|
93
94
|
intersectionObserver.observe(ref.current)
|
|
95
|
+
} else {
|
|
96
|
+
// If no ref is provided or ref.current isn't an HTML element,
|
|
97
|
+
// pass true to always subscribe since we can't track visibility
|
|
98
|
+
observer.next(true)
|
|
94
99
|
}
|
|
95
100
|
return () => intersectionObserver.disconnect()
|
|
96
101
|
})
|
|
@@ -115,9 +120,9 @@ export function usePreview({document: {_id, _type}, ref}: UsePreviewOptions): Us
|
|
|
115
120
|
// Create getSnapshot function to return current state
|
|
116
121
|
const getSnapshot = useCallback(() => {
|
|
117
122
|
const currentState = stateSource.getCurrent()
|
|
118
|
-
if (currentState.data === null) throw resolvePreview(instance,
|
|
123
|
+
if (currentState.data === null) throw resolvePreview(instance, docHandle)
|
|
119
124
|
return currentState as UsePreviewResults
|
|
120
|
-
}, [
|
|
125
|
+
}, [docHandle, instance, stateSource])
|
|
121
126
|
|
|
122
127
|
return useSyncExternalStore(subscribe, getSnapshot)
|
|
123
128
|
}
|
|
@@ -44,8 +44,8 @@ vi.mock('../context/useSanityInstance', () => ({
|
|
|
44
44
|
}))
|
|
45
45
|
|
|
46
46
|
const mockDocument: DocumentHandle = {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
documentId: 'doc1',
|
|
48
|
+
documentType: 'exampleType',
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
interface ProjectionResult {
|
|
@@ -61,7 +61,7 @@ function TestComponent({
|
|
|
61
61
|
projection: ValidProjection
|
|
62
62
|
}) {
|
|
63
63
|
const ref = useRef(null)
|
|
64
|
-
const {data, isPending} = useProjection<ProjectionResult>({document, projection, ref})
|
|
64
|
+
const {data, isPending} = useProjection<ProjectionResult>({...document, projection, ref})
|
|
65
65
|
|
|
66
66
|
return (
|
|
67
67
|
<div ref={ref}>
|
|
@@ -215,4 +215,69 @@ describe('useProjection', () => {
|
|
|
215
215
|
expect(screen.getByText('Updated Title')).toBeInTheDocument()
|
|
216
216
|
expect(screen.getByText('Added Description')).toBeInTheDocument()
|
|
217
217
|
})
|
|
218
|
+
|
|
219
|
+
test('it subscribes immediately when no ref is provided', async () => {
|
|
220
|
+
getCurrent.mockReturnValue({
|
|
221
|
+
data: {title: 'Title', description: 'Description'},
|
|
222
|
+
isPending: false,
|
|
223
|
+
})
|
|
224
|
+
const eventsUnsubscribe = vi.fn()
|
|
225
|
+
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
226
|
+
|
|
227
|
+
function NoRefComponent({
|
|
228
|
+
projection,
|
|
229
|
+
...docHandle
|
|
230
|
+
}: DocumentHandle & {projection: ValidProjection}) {
|
|
231
|
+
const {data} = useProjection<ProjectionResult>({...docHandle, projection}) // No ref provided
|
|
232
|
+
return (
|
|
233
|
+
<div>
|
|
234
|
+
<h1>{data.title}</h1>
|
|
235
|
+
<p>{data.description}</p>
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
render(
|
|
241
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
242
|
+
<NoRefComponent {...mockDocument} projection="{title, description}" />
|
|
243
|
+
</Suspense>,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
// Should subscribe immediately without waiting for intersection
|
|
247
|
+
expect(subscribe).toHaveBeenCalled()
|
|
248
|
+
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('it subscribes immediately when ref.current is not an HTML element', async () => {
|
|
252
|
+
getCurrent.mockReturnValue({
|
|
253
|
+
data: {title: 'Title', description: 'Description'},
|
|
254
|
+
isPending: false,
|
|
255
|
+
})
|
|
256
|
+
const eventsUnsubscribe = vi.fn()
|
|
257
|
+
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
258
|
+
|
|
259
|
+
function NonHtmlRefComponent({
|
|
260
|
+
projection,
|
|
261
|
+
...docHandle
|
|
262
|
+
}: DocumentHandle & {projection: ValidProjection}) {
|
|
263
|
+
const ref = useRef({}) // ref.current is not an HTML element
|
|
264
|
+
const {data} = useProjection<ProjectionResult>({...docHandle, projection, ref})
|
|
265
|
+
return (
|
|
266
|
+
<div>
|
|
267
|
+
<h1>{data.title}</h1>
|
|
268
|
+
<p>{data.description}</p>
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
render(
|
|
274
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
275
|
+
<NonHtmlRefComponent {...mockDocument} projection="{title, description}" />
|
|
276
|
+
</Suspense>,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// Should subscribe immediately without waiting for intersection
|
|
280
|
+
expect(subscribe).toHaveBeenCalled()
|
|
281
|
+
expect(screen.getByText('Title')).toBeInTheDocument()
|
|
282
|
+
})
|
|
218
283
|
})
|