@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.31

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.
Files changed (131) hide show
  1. package/README.md +6 -100
  2. package/dist/index.d.ts +2390 -2
  3. package/dist/index.js +1119 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +35 -49
  6. package/src/_exports/index.ts +2 -10
  7. package/src/_exports/sdk-react.ts +73 -0
  8. package/src/components/SDKProvider.test.tsx +103 -0
  9. package/src/components/SDKProvider.tsx +52 -0
  10. package/src/components/SanityApp.test.tsx +244 -0
  11. package/src/components/SanityApp.tsx +106 -0
  12. package/src/components/auth/AuthBoundary.test.tsx +204 -29
  13. package/src/components/auth/AuthBoundary.tsx +96 -19
  14. package/src/components/auth/ConfigurationError.ts +22 -0
  15. package/src/components/auth/LoginCallback.test.tsx +22 -24
  16. package/src/components/auth/LoginCallback.tsx +6 -16
  17. package/src/components/auth/LoginError.test.tsx +11 -18
  18. package/src/components/auth/LoginError.tsx +43 -25
  19. package/src/components/utils.ts +22 -0
  20. package/src/context/ResourceProvider.test.tsx +157 -0
  21. package/src/context/ResourceProvider.tsx +111 -0
  22. package/src/context/SanityInstanceContext.ts +4 -0
  23. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  24. package/src/hooks/auth/useAuthState.tsx +4 -5
  25. package/src/hooks/auth/useAuthToken.tsx +1 -1
  26. package/src/hooks/auth/useCurrentUser.tsx +28 -4
  27. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  28. package/src/hooks/auth/useDashboardOrganizationId.tsx +30 -0
  29. package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
  30. package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
  31. package/src/hooks/auth/useLogOut.test.tsx +2 -2
  32. package/src/hooks/auth/useLogOut.tsx +1 -1
  33. package/src/hooks/auth/useLoginUrl.tsx +14 -0
  34. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
  35. package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
  36. package/src/hooks/client/useClient.ts +13 -33
  37. package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
  38. package/src/hooks/comlink/useFrameConnection.ts +107 -0
  39. package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
  40. package/src/hooks/comlink/useManageFavorite.ts +210 -0
  41. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
  42. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
  43. package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
  44. package/src/hooks/comlink/useWindowConnection.ts +123 -0
  45. package/src/hooks/context/useSanityInstance.test.tsx +157 -15
  46. package/src/hooks/context/useSanityInstance.ts +68 -11
  47. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
  48. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
  49. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
  50. package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
  51. package/src/hooks/datasets/useDatasets.test.ts +80 -0
  52. package/src/hooks/datasets/useDatasets.ts +52 -0
  53. package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
  54. package/src/hooks/document/useApplyDocumentActions.ts +124 -0
  55. package/src/hooks/document/useDocument.test.ts +118 -0
  56. package/src/hooks/document/useDocument.ts +212 -0
  57. package/src/hooks/document/useDocumentEvent.test.ts +62 -0
  58. package/src/hooks/document/useDocumentEvent.ts +94 -0
  59. package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
  60. package/src/hooks/document/useDocumentPermissions.ts +131 -0
  61. package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
  62. package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
  63. package/src/hooks/document/useEditDocument.test.ts +196 -0
  64. package/src/hooks/document/useEditDocument.ts +314 -0
  65. package/src/hooks/documents/useDocuments.test.tsx +179 -0
  66. package/src/hooks/documents/useDocuments.ts +300 -0
  67. package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
  68. package/src/hooks/helpers/createCallbackHook.tsx +1 -1
  69. package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
  70. package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
  71. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
  72. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
  73. package/src/hooks/preview/usePreview.test.tsx +85 -17
  74. package/src/hooks/preview/usePreview.tsx +81 -22
  75. package/src/hooks/projection/useProjection.test.tsx +283 -0
  76. package/src/hooks/projection/useProjection.ts +232 -0
  77. package/src/hooks/projects/useProject.test.ts +80 -0
  78. package/src/hooks/projects/useProject.ts +51 -0
  79. package/src/hooks/projects/useProjects.test.ts +77 -0
  80. package/src/hooks/projects/useProjects.ts +45 -0
  81. package/src/hooks/query/useQuery.test.tsx +188 -0
  82. package/src/hooks/query/useQuery.ts +193 -0
  83. package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
  84. package/src/hooks/releases/useActiveReleases.ts +39 -0
  85. package/src/hooks/releases/usePerspective.test.tsx +120 -0
  86. package/src/hooks/releases/usePerspective.ts +49 -0
  87. package/src/hooks/users/useUsers.test.tsx +330 -0
  88. package/src/hooks/users/useUsers.ts +120 -0
  89. package/src/utils/getEnv.ts +21 -0
  90. package/src/version.ts +8 -0
  91. package/src/vite-env.d.ts +10 -0
  92. package/dist/_chunks-es/useLogOut.js +0 -44
  93. package/dist/_chunks-es/useLogOut.js.map +0 -1
  94. package/dist/assets/bundle-CcAyERuZ.css +0 -11
  95. package/dist/components.d.ts +0 -259
  96. package/dist/components.js +0 -301
  97. package/dist/components.js.map +0 -1
  98. package/dist/hooks.d.ts +0 -186
  99. package/dist/hooks.js +0 -81
  100. package/dist/hooks.js.map +0 -1
  101. package/src/_exports/components.ts +0 -13
  102. package/src/_exports/hooks.ts +0 -9
  103. package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
  104. package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
  105. package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
  106. package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
  107. package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
  108. package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
  109. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
  110. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
  111. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
  112. package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
  113. package/src/components/Login/LoginLinks.test.tsx +0 -100
  114. package/src/components/Login/LoginLinks.tsx +0 -73
  115. package/src/components/auth/Login.test.tsx +0 -41
  116. package/src/components/auth/Login.tsx +0 -45
  117. package/src/components/auth/LoginFooter.test.tsx +0 -29
  118. package/src/components/auth/LoginFooter.tsx +0 -65
  119. package/src/components/auth/LoginLayout.test.tsx +0 -33
  120. package/src/components/auth/LoginLayout.tsx +0 -81
  121. package/src/components/context/SanityProvider.test.tsx +0 -25
  122. package/src/components/context/SanityProvider.tsx +0 -42
  123. package/src/css/css.config.js +0 -220
  124. package/src/css/paramour.css +0 -2347
  125. package/src/css/styles.css +0 -11
  126. package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
  127. package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
  128. package/src/hooks/auth/useLoginUrls.tsx +0 -51
  129. package/src/hooks/client/useClient.test.tsx +0 -130
  130. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  131. package/src/hooks/documentCollection/useDocuments.ts +0 -87
@@ -0,0 +1,284 @@
1
+ import {act, renderHook} from '@testing-library/react'
2
+ import {describe, vi} from 'vitest'
3
+
4
+ import {ResourceProvider} from '../../context/ResourceProvider'
5
+ import {evaluateSync, parse} from '../_synchronous-groq-js.mjs'
6
+ import {useQuery} from '../query/useQuery'
7
+ import {usePaginatedDocuments} from './usePaginatedDocuments'
8
+
9
+ vi.mock('../query/useQuery')
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
+
18
+ beforeEach(() => {
19
+ const dataset = [
20
+ {
21
+ _id: 'movie1',
22
+ _type: 'movie',
23
+ title: 'The Matrix',
24
+ releaseYear: 1999,
25
+ _createdAt: '2021-03-09T00:00:00.000Z',
26
+ _updatedAt: '2021-03-09T00:00:00.000Z',
27
+ _rev: 'tx0',
28
+ },
29
+ {
30
+ _id: 'movie2',
31
+ _type: 'movie',
32
+ title: 'Inception',
33
+ releaseYear: 2010,
34
+ _createdAt: '2021-03-10T00:00:00.000Z',
35
+ _updatedAt: '2021-03-10T00:00:00.000Z',
36
+ _rev: 'tx1',
37
+ },
38
+ {
39
+ _id: 'movie3',
40
+ _type: 'movie',
41
+ title: 'Interstellar',
42
+ releaseYear: 2014,
43
+ _createdAt: '2021-03-11T00:00:00.000Z',
44
+ _updatedAt: '2021-03-11T00:00:00.000Z',
45
+ _rev: 'tx2',
46
+ },
47
+ {
48
+ _id: 'book1',
49
+ _type: 'book',
50
+ title: 'Dune',
51
+ _createdAt: '2021-03-12T00:00:00.000Z',
52
+ _updatedAt: '2021-03-12T00:00:00.000Z',
53
+ _rev: 'tx3',
54
+ },
55
+ {
56
+ _id: 'movie4',
57
+ _type: 'movie',
58
+ title: 'The Dark Knight',
59
+ releaseYear: 2008,
60
+ _createdAt: '2021-03-13T00:00:00.000Z',
61
+ _updatedAt: '2021-03-13T00:00:00.000Z',
62
+ _rev: 'tx4',
63
+ },
64
+ {
65
+ _id: 'movie5',
66
+ _type: 'movie',
67
+ title: 'Pulp Fiction',
68
+ releaseYear: 1994,
69
+ _createdAt: '2021-03-14T00:00:00.000Z',
70
+ _updatedAt: '2021-03-14T00:00:00.000Z',
71
+ _rev: 'tx5',
72
+ },
73
+ ]
74
+
75
+ vi.mocked(useQuery).mockImplementation(({query, ...options}) => {
76
+ const result = evaluateSync(parse(query), {dataset, params: options?.params}).get()
77
+ return {
78
+ data: result,
79
+ isPending: false,
80
+ }
81
+ })
82
+ })
83
+
84
+ it('should respect custom page size', () => {
85
+ const customPageSize = 2
86
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize: customPageSize}), {wrapper})
87
+
88
+ expect(result.current.pageSize).toBe(customPageSize)
89
+ expect(result.current.data.length).toBeLessThanOrEqual(customPageSize)
90
+ })
91
+
92
+ it('should filter by document type', () => {
93
+ const {result} = renderHook(() => usePaginatedDocuments({filter: '_type == "movie"'}), {
94
+ wrapper,
95
+ })
96
+
97
+ expect(result.current.data.every((doc) => doc.documentType === 'movie')).toBe(true)
98
+ expect(result.current.count).toBe(5) // 5 movies in the dataset
99
+ })
100
+
101
+ // groq-js doesn't support search filters yet
102
+ it.skip('should apply search filter', () => {
103
+ const {result} = renderHook(() => usePaginatedDocuments({search: 'inter'}), {wrapper})
104
+
105
+ // Should match "Interstellar"
106
+ expect(result.current.data.some((doc) => doc.documentId === 'movie3')).toBe(true)
107
+ })
108
+
109
+ it('should apply ordering', () => {
110
+ const {result} = renderHook(
111
+ () =>
112
+ usePaginatedDocuments({
113
+ filter: '_type == "movie"',
114
+ orderings: [{field: 'releaseYear', direction: 'desc'}],
115
+ }),
116
+ {wrapper},
117
+ )
118
+
119
+ // First item should be the most recent movie (Interstellar, 2014)
120
+ expect(result.current.data[0].documentId).toBe('movie3')
121
+ })
122
+
123
+ it('should calculate pagination values correctly', () => {
124
+ const pageSize = 2
125
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
126
+
127
+ expect(result.current.currentPage).toBe(1)
128
+ expect(result.current.totalPages).toBe(3) // 6 items with page size 2
129
+ expect(result.current.startIndex).toBe(0)
130
+ expect(result.current.endIndex).toBe(2)
131
+ expect(result.current.count).toBe(6)
132
+ })
133
+
134
+ it('should navigate to next page', () => {
135
+ const pageSize = 2
136
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
137
+
138
+ expect(result.current.currentPage).toBe(1)
139
+ expect(result.current.data.length).toBe(pageSize)
140
+
141
+ act(() => {
142
+ result.current.nextPage()
143
+ })
144
+
145
+ expect(result.current.currentPage).toBe(2)
146
+ expect(result.current.startIndex).toBe(pageSize)
147
+ expect(result.current.endIndex).toBe(pageSize * 2)
148
+ })
149
+
150
+ it('should navigate to previous page', () => {
151
+ const pageSize = 2
152
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
153
+
154
+ // Go to page 2 first
155
+ act(() => {
156
+ result.current.nextPage()
157
+ })
158
+
159
+ expect(result.current.currentPage).toBe(2)
160
+
161
+ // Then go back to page 1
162
+ act(() => {
163
+ result.current.previousPage()
164
+ })
165
+
166
+ expect(result.current.currentPage).toBe(1)
167
+ expect(result.current.startIndex).toBe(0)
168
+ })
169
+
170
+ it('should navigate to first page', () => {
171
+ const pageSize = 2
172
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
173
+
174
+ // Go to last page first
175
+ act(() => {
176
+ result.current.lastPage()
177
+ })
178
+
179
+ expect(result.current.currentPage).toBe(3) // Last page (3rd page)
180
+
181
+ // Then go back to first page
182
+ act(() => {
183
+ result.current.firstPage()
184
+ })
185
+
186
+ expect(result.current.currentPage).toBe(1)
187
+ expect(result.current.startIndex).toBe(0)
188
+ })
189
+
190
+ it('should navigate to last page', () => {
191
+ const pageSize = 2
192
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
193
+
194
+ act(() => {
195
+ result.current.lastPage()
196
+ })
197
+
198
+ expect(result.current.currentPage).toBe(3) // Last page (3rd page)
199
+ expect(result.current.startIndex).toBe(4) // Index 4-5 for the last page
200
+ })
201
+
202
+ it('should navigate to specific page', () => {
203
+ const pageSize = 2
204
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
205
+
206
+ act(() => {
207
+ result.current.goToPage(2) // Go to page 2
208
+ })
209
+
210
+ expect(result.current.currentPage).toBe(2)
211
+ expect(result.current.startIndex).toBe(2) // Index 2-3 for page 2
212
+
213
+ // Should not navigate to invalid page numbers
214
+ act(() => {
215
+ result.current.goToPage(0) // Invalid page
216
+ })
217
+
218
+ expect(result.current.currentPage).toBe(2) // Should remain on page 2
219
+
220
+ act(() => {
221
+ result.current.goToPage(10) // Invalid page
222
+ })
223
+
224
+ expect(result.current.currentPage).toBe(2) // Should remain on page 2
225
+ })
226
+
227
+ it('should set page availability flags correctly', () => {
228
+ const pageSize = 2
229
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
230
+ // On first page
231
+ expect(result.current.hasFirstPage).toBe(false)
232
+ expect(result.current.hasPreviousPage).toBe(false)
233
+ expect(result.current.hasNextPage).toBe(true)
234
+ expect(result.current.hasLastPage).toBe(true)
235
+ // Go to middle page
236
+ act(() => {
237
+ result.current.nextPage()
238
+ })
239
+ expect(result.current.hasFirstPage).toBe(true)
240
+ expect(result.current.hasPreviousPage).toBe(true)
241
+ expect(result.current.hasNextPage).toBe(true)
242
+ expect(result.current.hasLastPage).toBe(true)
243
+ // Go to last page
244
+ act(() => {
245
+ result.current.lastPage()
246
+ })
247
+ expect(result.current.hasFirstPage).toBe(true)
248
+ expect(result.current.hasPreviousPage).toBe(true)
249
+ expect(result.current.hasNextPage).toBe(false)
250
+ expect(result.current.hasLastPage).toBe(false)
251
+ })
252
+
253
+ // New test case for resetting the current page when filter changes
254
+ it('should reset current page when filter changes', () => {
255
+ const {result, rerender} = renderHook((props) => usePaginatedDocuments(props), {
256
+ initialProps: {pageSize: 2, filter: ''},
257
+ wrapper,
258
+ })
259
+ // Initially, current page should be 1
260
+ expect(result.current.currentPage).toBe(1)
261
+ // Navigate to next page
262
+ act(() => {
263
+ result.current.nextPage()
264
+ })
265
+ expect(result.current.currentPage).toBe(2)
266
+ // Now update filter, which should reset the page to the first page
267
+ rerender({pageSize: 2, filter: '_type == "movie"'})
268
+ expect(result.current.currentPage).toBe(1)
269
+ expect(result.current.startIndex).toBe(0)
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
+ })
284
+ })
@@ -0,0 +1,353 @@
1
+ import {createGroqSearchFilter, type DocumentHandle, type QueryOptions} from '@sanity/sdk'
2
+ import {type SortOrderingItem} from '@sanity/types'
3
+ import {pick} from 'lodash-es'
4
+ import {useCallback, useEffect, useMemo, useState} from 'react'
5
+
6
+ import {useSanityInstance} from '../context/useSanityInstance'
7
+ import {useQuery} from '../query/useQuery'
8
+
9
+ /**
10
+ * Configuration options for the usePaginatedDocuments hook
11
+ *
12
+ * @beta
13
+ * @category Types
14
+ */
15
+ export interface PaginatedDocumentsOptions<
16
+ TDocumentType extends string = string,
17
+ TDataset extends string = string,
18
+ TProjectId extends string = string,
19
+ > extends Omit<QueryOptions<TDocumentType, TDataset, TProjectId>, 'query'> {
20
+ documentType?: TDocumentType | TDocumentType[]
21
+ /**
22
+ * GROQ filter expression to apply to the query
23
+ */
24
+ filter?: string
25
+ /**
26
+ * Number of items to display per page (defaults to 25)
27
+ */
28
+ pageSize?: number
29
+ /**
30
+ * Sorting configuration for the results
31
+ */
32
+ orderings?: SortOrderingItem[]
33
+ /**
34
+ * Text search query to filter results
35
+ */
36
+ search?: string
37
+ }
38
+
39
+ /**
40
+ * Return value from the usePaginatedDocuments hook
41
+ *
42
+ * @beta
43
+ * @category Types
44
+ */
45
+ export interface PaginatedDocumentsResponse<
46
+ TDocumentType extends string = string,
47
+ TDataset extends string = string,
48
+ TProjectId extends string = string,
49
+ > {
50
+ /**
51
+ * Array of document handles for the current page
52
+ */
53
+ data: DocumentHandle<TDocumentType, TDataset, TProjectId>[]
54
+ /**
55
+ * Whether a query is currently in progress
56
+ */
57
+ isPending: boolean
58
+
59
+ /**
60
+ * Number of items displayed per page
61
+ */
62
+ pageSize: number
63
+ /**
64
+ * Current page number (1-indexed)
65
+ */
66
+ currentPage: number
67
+ /**
68
+ * Total number of pages available
69
+ */
70
+ totalPages: number
71
+
72
+ /**
73
+ * Starting index of the current page (0-indexed)
74
+ */
75
+ startIndex: number
76
+ /**
77
+ * Ending index of the current page (exclusive, 0-indexed)
78
+ */
79
+ endIndex: number
80
+ /**
81
+ * Total count of items matching the query
82
+ */
83
+ count: number
84
+
85
+ /**
86
+ * Navigate to the first page
87
+ */
88
+ firstPage: () => void
89
+ /**
90
+ * Whether there is a first page available to navigate to
91
+ */
92
+ hasFirstPage: boolean
93
+
94
+ /**
95
+ * Navigate to the previous page
96
+ */
97
+ previousPage: () => void
98
+ /**
99
+ * Whether there is a previous page available to navigate to
100
+ */
101
+ hasPreviousPage: boolean
102
+
103
+ /**
104
+ * Navigate to the next page
105
+ */
106
+ nextPage: () => void
107
+ /**
108
+ * Whether there is a next page available to navigate to
109
+ */
110
+ hasNextPage: boolean
111
+
112
+ /**
113
+ * Navigate to the last page
114
+ */
115
+ lastPage: () => void
116
+ /**
117
+ * Whether there is a last page available to navigate to
118
+ */
119
+ hasLastPage: boolean
120
+
121
+ /**
122
+ * Navigate to a specific page number
123
+ * @param pageNumber - The page number to navigate to (1-indexed)
124
+ */
125
+ goToPage: (pageNumber: number) => void
126
+ }
127
+
128
+ /**
129
+ * Retrieves pages of {@link DocumentHandle}s, narrowed by optional filters, text searches, and custom ordering,
130
+ * with support for traditional paginated interfaces. The number of document handles returned per page is customizable,
131
+ * while page navigation is handled via the included navigation functions.
132
+ *
133
+ * @beta
134
+ * @category Documents
135
+ * @param options - Configuration options for the paginated list
136
+ * @returns An object containing the list of document handles, pagination details, and functions to navigate between pages
137
+ *
138
+ * @remarks
139
+ * - The returned document handles include projectId and dataset information from the current Sanity instance
140
+ * - This makes them ready to use with document operations and other document hooks
141
+ * - The hook automatically uses the correct Sanity instance based on the projectId and dataset in the options
142
+ *
143
+ * @example Paginated list of documents with navigation
144
+ * ```tsx
145
+ * import {
146
+ * usePaginatedDocuments,
147
+ * createDatasetHandle,
148
+ * type DatasetHandle,
149
+ * type DocumentHandle,
150
+ * type SortOrderingItem,
151
+ * useProjection
152
+ * } from '@sanity/sdk-react'
153
+ * import {Suspense} from 'react'
154
+ * import {ErrorBoundary} from 'react-error-boundary'
155
+ *
156
+ * // Define a component to display a single document row
157
+ * function MyTableRowComponent({doc}: {doc: DocumentHandle}) {
158
+ * const {data} = useProjection<{title?: string}>({
159
+ * ...doc,
160
+ * projection: '{title}',
161
+ * })
162
+ *
163
+ * return (
164
+ * <tr>
165
+ * <td>{data?.title ?? 'Untitled'}</td>
166
+ * </tr>
167
+ * )
168
+ * }
169
+ *
170
+ * // Define props for the list component
171
+ * interface PaginatedDocumentListProps {
172
+ * documentType: string
173
+ * dataset?: DatasetHandle
174
+ * }
175
+ *
176
+ * function PaginatedDocumentList({documentType, dataset}: PaginatedDocumentListProps) {
177
+ * const {
178
+ * data,
179
+ * isPending,
180
+ * currentPage,
181
+ * totalPages,
182
+ * nextPage,
183
+ * previousPage,
184
+ * hasNextPage,
185
+ * hasPreviousPage
186
+ * } = usePaginatedDocuments({
187
+ * ...dataset,
188
+ * documentType,
189
+ * pageSize: 10,
190
+ * orderings: [{field: '_createdAt', direction: 'desc'}],
191
+ * })
192
+ *
193
+ * return (
194
+ * <div>
195
+ * <table>
196
+ * <thead>
197
+ * <tr><th>Title</th></tr>
198
+ * </thead>
199
+ * <tbody>
200
+ * {data.map(doc => (
201
+ * <ErrorBoundary key={doc.documentId} fallback={<tr><td>Error loading document</td></tr>}>
202
+ * <Suspense fallback={<tr><td>Loading...</td></tr>}>
203
+ * <MyTableRowComponent doc={doc} />
204
+ * </Suspense>
205
+ * </ErrorBoundary>
206
+ * ))}
207
+ * </tbody>
208
+ * </table>
209
+ * <div style={{opacity: isPending ? 0.5 : 1}}>
210
+ * <button onClick={previousPage} disabled={!hasPreviousPage || isPending}>Previous</button>
211
+ * <span>Page {currentPage} / {totalPages}</span>
212
+ * <button onClick={nextPage} disabled={!hasNextPage || isPending}>Next</button>
213
+ * </div>
214
+ * </div>
215
+ * )
216
+ * }
217
+ *
218
+ * // Usage:
219
+ * // const myDatasetHandle = createDatasetHandle({ projectId: 'p1', dataset: 'production' })
220
+ * // <PaginatedDocumentList dataset={myDatasetHandle} documentType="post" />
221
+ * ```
222
+ */
223
+ export function usePaginatedDocuments<
224
+ TDocumentType extends string = string,
225
+ TDataset extends string = string,
226
+ TProjectId extends string = string,
227
+ >({
228
+ documentType,
229
+ filter = '',
230
+ pageSize = 25,
231
+ params = {},
232
+ orderings,
233
+ search,
234
+ ...options
235
+ }: PaginatedDocumentsOptions<TDocumentType, TDataset, TProjectId>): PaginatedDocumentsResponse<
236
+ TDocumentType,
237
+ TDataset,
238
+ TProjectId
239
+ > {
240
+ const instance = useSanityInstance(options)
241
+ const [pageIndex, setPageIndex] = useState(0)
242
+ const key = JSON.stringify({filter, search, params, orderings, pageSize})
243
+ // Reset the pageIndex to 0 whenever any query parameters (filter, search,
244
+ // params, orderings) or pageSize changes
245
+ useEffect(() => {
246
+ setPageIndex(0)
247
+ }, [key])
248
+
249
+ const startIndex = pageIndex * pageSize
250
+ const endIndex = (pageIndex + 1) * pageSize
251
+ const documentTypes = (Array.isArray(documentType) ? documentType : [documentType]).filter(
252
+ (i) => typeof i === 'string',
253
+ )
254
+
255
+ const filterClause = useMemo(() => {
256
+ const conditions: string[] = []
257
+ const trimmedSearch = search?.trim()
258
+
259
+ // Add search query filter if specified
260
+ if (trimmedSearch) {
261
+ const searchFilter = createGroqSearchFilter(trimmedSearch)
262
+ if (searchFilter) {
263
+ conditions.push(searchFilter)
264
+ }
265
+ }
266
+
267
+ if (documentTypes?.length) {
268
+ conditions.push(`(_type in $__types)`)
269
+ }
270
+
271
+ // Add additional filter if specified
272
+ if (filter) {
273
+ conditions.push(`(${filter})`)
274
+ }
275
+
276
+ return conditions.length ? `[${conditions.join(' && ')}]` : ''
277
+ }, [filter, search, documentTypes?.length])
278
+
279
+ const orderClause = orderings
280
+ ? `| order(${orderings
281
+ .map((ordering) =>
282
+ [ordering.field, ordering.direction.toLowerCase()]
283
+ .map((str) => str.trim())
284
+ .filter(Boolean)
285
+ .join(' '),
286
+ )
287
+ .join(',')})`
288
+ : ''
289
+
290
+ const dataQuery = `*${filterClause}${orderClause}[${startIndex}...${endIndex}]{"documentId":_id,"documentType":_type,...$__handle}`
291
+ const countQuery = `count(*${filterClause})`
292
+
293
+ const {
294
+ data: {data, count},
295
+ isPending,
296
+ } = useQuery<{data: DocumentHandle<TDocumentType, TDataset, TProjectId>[]; count: number}>({
297
+ ...options,
298
+ query: `{"data":${dataQuery},"count":${countQuery}}`,
299
+ params: {
300
+ ...params,
301
+ __types: documentTypes,
302
+ __handle: {
303
+ ...pick(instance.config, 'projectId', 'dataset', 'perspective'),
304
+ ...pick(options, 'projectId', 'dataset', 'perspective'),
305
+ },
306
+ },
307
+ })
308
+
309
+ const totalPages = Math.ceil(count / pageSize)
310
+ const currentPage = pageIndex + 1
311
+
312
+ // Navigation methods
313
+ const firstPage = useCallback(() => setPageIndex(0), [])
314
+ const previousPage = useCallback(() => setPageIndex((prev) => Math.max(prev - 1, 0)), [])
315
+ const nextPage = useCallback(
316
+ () => setPageIndex((prev) => Math.min(prev + 1, totalPages - 1)),
317
+ [totalPages],
318
+ )
319
+ const lastPage = useCallback(() => setPageIndex(totalPages - 1), [totalPages])
320
+ const goToPage = useCallback(
321
+ (pageNumber: number) => {
322
+ if (pageNumber < 1 || pageNumber > totalPages) return
323
+ setPageIndex(pageNumber - 1)
324
+ },
325
+ [totalPages],
326
+ )
327
+
328
+ // Boolean flags for page availability
329
+ const hasFirstPage = pageIndex > 0
330
+ const hasPreviousPage = pageIndex > 0
331
+ const hasNextPage = pageIndex < totalPages - 1
332
+ const hasLastPage = pageIndex < totalPages - 1
333
+
334
+ return {
335
+ data,
336
+ isPending,
337
+ pageSize,
338
+ currentPage,
339
+ totalPages,
340
+ startIndex,
341
+ endIndex,
342
+ count,
343
+ firstPage,
344
+ hasFirstPage,
345
+ previousPage,
346
+ hasPreviousPage,
347
+ nextPage,
348
+ hasNextPage,
349
+ lastPage,
350
+ hasLastPage,
351
+ goToPage,
352
+ }
353
+ }