@sanity/sdk-react 0.0.0-alpha.15 → 0.0.0-alpha.16

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 (43) hide show
  1. package/dist/_chunks-es/useLogOut.js +1 -1
  2. package/dist/_chunks-es/useLogOut.js.map +1 -1
  3. package/dist/components.d.ts +37 -9
  4. package/dist/components.js +7 -8
  5. package/dist/components.js.map +1 -1
  6. package/dist/hooks.d.ts +390 -141
  7. package/dist/hooks.js +160 -75
  8. package/dist/hooks.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +10 -2
  11. package/dist/index.js.map +1 -1
  12. package/package.json +6 -6
  13. package/src/_exports/hooks.ts +14 -5
  14. package/src/_exports/index.ts +1 -10
  15. package/src/components/SDKProvider.test.tsx +3 -3
  16. package/src/components/SDKProvider.tsx +18 -7
  17. package/src/components/SanityApp.test.tsx +5 -5
  18. package/src/components/SanityApp.tsx +36 -9
  19. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  20. package/src/hooks/client/useClient.ts +4 -1
  21. package/src/hooks/context/useSanityInstance.ts +1 -1
  22. package/src/hooks/datasets/useDatasets.ts +26 -1
  23. package/src/hooks/document/useDocument.ts +3 -3
  24. package/src/hooks/document/useDocumentEvent.ts +2 -0
  25. package/src/hooks/document/useDocumentSyncStatus.ts +2 -1
  26. package/src/hooks/document/useEditDocument.ts +2 -1
  27. package/src/hooks/helpers/createStateSourceHook.tsx +4 -4
  28. package/src/hooks/infiniteList/useInfiniteList.test.tsx +152 -0
  29. package/src/hooks/infiniteList/useInfiniteList.ts +163 -0
  30. package/src/hooks/paginatedList/usePaginatedList.test.tsx +259 -0
  31. package/src/hooks/paginatedList/usePaginatedList.ts +278 -0
  32. package/src/hooks/projects/useProject.ts +25 -1
  33. package/src/hooks/projects/useProjects.ts +33 -11
  34. package/src/hooks/query/useQuery.test.tsx +188 -0
  35. package/src/hooks/query/useQuery.ts +112 -0
  36. package/src/hooks/users/useUsers.ts +2 -2
  37. package/src/utils/getEnv.ts +21 -0
  38. package/src/version.ts +8 -0
  39. package/src/hooks/documentCollection/types.ts +0 -19
  40. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  41. package/src/hooks/documentCollection/useDocuments.ts +0 -126
  42. package/src/hooks/documentCollection/useSearch.test.ts +0 -100
  43. package/src/hooks/documentCollection/useSearch.ts +0 -75
@@ -0,0 +1,152 @@
1
+ import {act, renderHook} from '@testing-library/react'
2
+ import {describe, vi} from 'vitest'
3
+
4
+ import {evaluateSync, parse} from '../_synchronous-groq-js.mjs'
5
+ import {useQuery} from '../query/useQuery'
6
+ import {useInfiniteList} from './useInfiniteList'
7
+
8
+ vi.mock('../query/useQuery')
9
+
10
+ describe('useInfiniteList', () => {
11
+ beforeEach(() => {
12
+ const dataset = [
13
+ {
14
+ _id: 'movie1',
15
+ _type: 'movie',
16
+ title: 'The Matrix',
17
+ releaseYear: 1999,
18
+ _createdAt: '2021-03-09T00:00:00.000Z',
19
+ _updatedAt: '2021-03-09T00:00:00.000Z',
20
+ _rev: 'tx0',
21
+ },
22
+ {
23
+ _id: 'movie2',
24
+ _type: 'movie',
25
+ title: 'Inception',
26
+ releaseYear: 2010,
27
+ _createdAt: '2021-03-10T00:00:00.000Z',
28
+ _updatedAt: '2021-03-10T00:00:00.000Z',
29
+ _rev: 'tx1',
30
+ },
31
+ {
32
+ _id: 'movie3',
33
+ _type: 'movie',
34
+ title: 'Interstellar',
35
+ releaseYear: 2014,
36
+ _createdAt: '2021-03-11T00:00:00.000Z',
37
+ _updatedAt: '2021-03-11T00:00:00.000Z',
38
+ _rev: 'tx2',
39
+ },
40
+ {
41
+ _id: 'book1',
42
+ _type: 'book',
43
+ title: 'Dune',
44
+ _createdAt: '2021-03-12T00:00:00.000Z',
45
+ _updatedAt: '2021-03-12T00:00:00.000Z',
46
+ _rev: 'tx3',
47
+ },
48
+ {
49
+ _id: 'movie4',
50
+ _type: 'movie',
51
+ title: 'The Dark Knight',
52
+ releaseYear: 2008,
53
+ _createdAt: '2021-03-13T00:00:00.000Z',
54
+ _updatedAt: '2021-03-13T00:00:00.000Z',
55
+ _rev: 'tx4',
56
+ },
57
+ {
58
+ _id: 'movie5',
59
+ _type: 'movie',
60
+ title: 'Pulp Fiction',
61
+ releaseYear: 1994,
62
+ _createdAt: '2021-03-14T00:00:00.000Z',
63
+ _updatedAt: '2021-03-14T00:00:00.000Z',
64
+ _rev: 'tx5',
65
+ },
66
+ ]
67
+
68
+ vi.mocked(useQuery).mockImplementation((query, options) => {
69
+ const result = evaluateSync(parse(query), {dataset, params: options?.params}).get()
70
+ return {
71
+ data: result,
72
+ isPending: false,
73
+ }
74
+ })
75
+ })
76
+
77
+ it('should respect custom page size', () => {
78
+ const customPageSize = 2
79
+ const {result} = renderHook(() => useInfiniteList({pageSize: customPageSize}))
80
+
81
+ expect(result.current.data.length).toBe(customPageSize)
82
+ })
83
+
84
+ it('should filter by document type', () => {
85
+ const {result} = renderHook(() => useInfiniteList({filter: '_type == "movie"'}))
86
+
87
+ expect(result.current.data.every((doc) => doc._type === 'movie')).toBe(true)
88
+ expect(result.current.count).toBe(5) // 5 movies in the dataset
89
+ })
90
+
91
+ // groq-js doesn't support search filters yet
92
+ it.skip('should apply search filter', () => {
93
+ const {result} = renderHook(() => useInfiniteList({search: 'inter'}))
94
+
95
+ // Should match "Interstellar"
96
+ expect(result.current.data.some((doc) => doc._id === 'movie3')).toBe(true)
97
+ })
98
+
99
+ it('should apply ordering', () => {
100
+ const {result} = renderHook(() =>
101
+ useInfiniteList({
102
+ filter: '_type == "movie"',
103
+ orderings: [{field: 'releaseYear', direction: 'desc'}],
104
+ }),
105
+ )
106
+
107
+ // First item should be the most recent movie (Interstellar, 2014)
108
+ expect(result.current.data[0]._id).toBe('movie3')
109
+ })
110
+
111
+ it('should load more data when loadMore is called', () => {
112
+ const pageSize = 2
113
+ const {result} = renderHook(() => useInfiniteList({pageSize}))
114
+
115
+ expect(result.current.data.length).toBe(pageSize)
116
+
117
+ act(() => {
118
+ result.current.loadMore()
119
+ })
120
+
121
+ expect(result.current.data.length).toBe(pageSize * 2)
122
+ })
123
+
124
+ it('should indicate when there is more data to load', () => {
125
+ const {result} = renderHook(() => useInfiniteList({pageSize: 3}))
126
+ expect(result.current.hasMore).toBe(true)
127
+ // Load all remaining data
128
+ act(() => {
129
+ result.current.loadMore()
130
+ })
131
+ expect(result.current.hasMore).toBe(false)
132
+ })
133
+
134
+ // New test case for resetting limit when filter changes
135
+ it('should reset limit when filter changes', () => {
136
+ const {result, rerender} = renderHook((props) => useInfiniteList(props), {
137
+ initialProps: {pageSize: 2, filter: ''},
138
+ })
139
+ // Initially, data length equals pageSize (2)
140
+ expect(result.current.data.length).toBe(2)
141
+ // Load more to increase limit
142
+ act(() => {
143
+ result.current.loadMore()
144
+ })
145
+ // After loadMore, data length should be increased (2 + 2 = 4)
146
+ expect(result.current.data.length).toBe(4)
147
+ // Now update filter to trigger resetting the limit
148
+ rerender({pageSize: 2, filter: '_type == "movie"'})
149
+ // With the filter applied, the limit is reset to pageSize (i.e. 2)
150
+ expect(result.current.data.length).toBe(2)
151
+ })
152
+ })
@@ -0,0 +1,163 @@
1
+ import {type DocumentHandle, type QueryOptions} from '@sanity/sdk'
2
+ import {type SortOrderingItem} from '@sanity/types'
3
+ import {useCallback, useEffect, useMemo, useState} from 'react'
4
+
5
+ import {useQuery} from '../query/useQuery'
6
+
7
+ const DEFAULT_PAGE_SIZE = 25
8
+ const DEFAULT_PERSPECTIVE = 'drafts'
9
+
10
+ /**
11
+ * Result structure returned from the infinite list query
12
+ * @internal
13
+ */
14
+ interface InfiniteListQueryResult {
15
+ count: number
16
+ data: DocumentHandle[]
17
+ }
18
+
19
+ /**
20
+ * Configuration options for the useInfiniteList hook
21
+ *
22
+ * @beta
23
+ */
24
+ export interface InfiniteListOptions extends QueryOptions {
25
+ /**
26
+ * GROQ filter expression to apply to the query
27
+ */
28
+ filter?: string
29
+ /**
30
+ * Number of items to load per page (defaults to 25)
31
+ */
32
+ pageSize?: number
33
+ /**
34
+ * Sorting configuration for the results
35
+ */
36
+ orderings?: SortOrderingItem[]
37
+ /**
38
+ * Text search query to filter results
39
+ */
40
+ search?: string
41
+ }
42
+
43
+ /**
44
+ * Return value from the useInfiniteList hook
45
+ *
46
+ * @beta
47
+ */
48
+ export interface InfiniteList {
49
+ /**
50
+ * Array of document handles for the current page
51
+ */
52
+ data: DocumentHandle[]
53
+ /**
54
+ * Whether there are more items available to load
55
+ */
56
+ hasMore: boolean
57
+ /**
58
+ * Total count of items matching the query
59
+ */
60
+ count: number
61
+ /**
62
+ * Whether a query is currently in progress
63
+ */
64
+ isPending: boolean
65
+ /**
66
+ * Function to load the next page of results
67
+ */
68
+ loadMore: () => void
69
+ }
70
+
71
+ /**
72
+ * React hook for paginated document queries with infinite scrolling support
73
+ *
74
+ * This hook provides a convenient way to implement infinite scrolling lists of documents
75
+ * with support for filtering, searching, and custom ordering. It handles pagination
76
+ * automatically and provides a simple API for loading more results.
77
+ *
78
+ * The hook constructs and executes GROQ queries based on the provided options,
79
+ * combining search terms, filters, and ordering specifications. It maintains the
80
+ * current page size internally and exposes a function to load additional items.
81
+ *
82
+ * Usage example:
83
+ * ```tsx
84
+ * const {data, hasMore, isPending, loadMore} = useInfiniteList({
85
+ * filter: '_type == "post"',
86
+ * search: searchTerm,
87
+ * pageSize: 10,
88
+ * orderings: [{field: '_createdAt', direction: 'desc'}]
89
+ * })
90
+ * ```
91
+ *
92
+ * @beta
93
+ * @param options - Configuration options for the infinite list
94
+ * @returns An object containing the current data, loading state, and functions to load more
95
+ */
96
+ export function useInfiniteList({
97
+ pageSize = DEFAULT_PAGE_SIZE,
98
+ params,
99
+ search,
100
+ filter,
101
+ orderings,
102
+ ...options
103
+ }: InfiniteListOptions): InfiniteList {
104
+ const perspective = options.perspective ?? DEFAULT_PERSPECTIVE
105
+ const [limit, setLimit] = useState(pageSize)
106
+
107
+ // Reset the limit to the current pageSize whenever any query parameters
108
+ // (filter, search, params, orderings) or pageSize changes
109
+ const key = JSON.stringify({filter, search, params, orderings, pageSize})
110
+ useEffect(() => {
111
+ setLimit(pageSize)
112
+ }, [key, pageSize])
113
+
114
+ const filterClause = useMemo(() => {
115
+ const conditions: string[] = []
116
+
117
+ // Add search query if specified
118
+ if (search?.trim()) {
119
+ conditions.push(`[@] match text::query("${search.trim()}")`)
120
+ }
121
+
122
+ // Add additional filter if specified
123
+ if (filter) {
124
+ conditions.push(`(${filter})`)
125
+ }
126
+
127
+ return conditions.length ? `[${conditions.join(' && ')}]` : ''
128
+ }, [filter, search])
129
+
130
+ const orderClause = orderings
131
+ ? `| order(${orderings
132
+ .map((ordering) =>
133
+ [ordering.field, ordering.direction.toLowerCase()]
134
+ .map((str) => str.trim())
135
+ .filter(Boolean)
136
+ .join(' '),
137
+ )
138
+ .join(',')})`
139
+ : ''
140
+
141
+ const dataQuery = `*${filterClause}${orderClause}[0...${limit}]{_id,_type}`
142
+ const countQuery = `count(*${filterClause})`
143
+
144
+ const {
145
+ data: {count, data},
146
+ isPending,
147
+ } = useQuery<InfiniteListQueryResult>(`{"count":${countQuery},"data":${dataQuery}}`, {
148
+ ...options,
149
+ params,
150
+ perspective,
151
+ })
152
+
153
+ const hasMore = data.length < count
154
+
155
+ const loadMore = useCallback(() => {
156
+ setLimit((prev) => Math.min(prev + pageSize, count))
157
+ }, [count, pageSize])
158
+
159
+ return useMemo(
160
+ () => ({data, hasMore, count, isPending, loadMore}),
161
+ [data, hasMore, count, isPending, loadMore],
162
+ )
163
+ }
@@ -0,0 +1,259 @@
1
+ import {act, renderHook} from '@testing-library/react'
2
+ import {describe, vi} from 'vitest'
3
+
4
+ import {evaluateSync, parse} from '../_synchronous-groq-js.mjs'
5
+ import {useQuery} from '../query/useQuery'
6
+ import {usePaginatedList} from './usePaginatedList'
7
+
8
+ vi.mock('../query/useQuery')
9
+
10
+ describe('usePaginatedList', () => {
11
+ beforeEach(() => {
12
+ const dataset = [
13
+ {
14
+ _id: 'movie1',
15
+ _type: 'movie',
16
+ title: 'The Matrix',
17
+ releaseYear: 1999,
18
+ _createdAt: '2021-03-09T00:00:00.000Z',
19
+ _updatedAt: '2021-03-09T00:00:00.000Z',
20
+ _rev: 'tx0',
21
+ },
22
+ {
23
+ _id: 'movie2',
24
+ _type: 'movie',
25
+ title: 'Inception',
26
+ releaseYear: 2010,
27
+ _createdAt: '2021-03-10T00:00:00.000Z',
28
+ _updatedAt: '2021-03-10T00:00:00.000Z',
29
+ _rev: 'tx1',
30
+ },
31
+ {
32
+ _id: 'movie3',
33
+ _type: 'movie',
34
+ title: 'Interstellar',
35
+ releaseYear: 2014,
36
+ _createdAt: '2021-03-11T00:00:00.000Z',
37
+ _updatedAt: '2021-03-11T00:00:00.000Z',
38
+ _rev: 'tx2',
39
+ },
40
+ {
41
+ _id: 'book1',
42
+ _type: 'book',
43
+ title: 'Dune',
44
+ _createdAt: '2021-03-12T00:00:00.000Z',
45
+ _updatedAt: '2021-03-12T00:00:00.000Z',
46
+ _rev: 'tx3',
47
+ },
48
+ {
49
+ _id: 'movie4',
50
+ _type: 'movie',
51
+ title: 'The Dark Knight',
52
+ releaseYear: 2008,
53
+ _createdAt: '2021-03-13T00:00:00.000Z',
54
+ _updatedAt: '2021-03-13T00:00:00.000Z',
55
+ _rev: 'tx4',
56
+ },
57
+ {
58
+ _id: 'movie5',
59
+ _type: 'movie',
60
+ title: 'Pulp Fiction',
61
+ releaseYear: 1994,
62
+ _createdAt: '2021-03-14T00:00:00.000Z',
63
+ _updatedAt: '2021-03-14T00:00:00.000Z',
64
+ _rev: 'tx5',
65
+ },
66
+ ]
67
+
68
+ vi.mocked(useQuery).mockImplementation((query, options) => {
69
+ const result = evaluateSync(parse(query), {dataset, params: options?.params}).get()
70
+ return {
71
+ data: result,
72
+ isPending: false,
73
+ }
74
+ })
75
+ })
76
+
77
+ it('should respect custom page size', () => {
78
+ const customPageSize = 2
79
+ const {result} = renderHook(() => usePaginatedList({pageSize: customPageSize}))
80
+
81
+ expect(result.current.pageSize).toBe(customPageSize)
82
+ expect(result.current.data.length).toBeLessThanOrEqual(customPageSize)
83
+ })
84
+
85
+ it('should filter by document type', () => {
86
+ const {result} = renderHook(() => usePaginatedList({filter: '_type == "movie"'}))
87
+
88
+ expect(result.current.data.every((doc) => doc._type === 'movie')).toBe(true)
89
+ expect(result.current.count).toBe(5) // 5 movies in the dataset
90
+ })
91
+
92
+ // groq-js doesn't support search filters yet
93
+ it.skip('should apply search filter', () => {
94
+ const {result} = renderHook(() => usePaginatedList({search: 'inter'}))
95
+
96
+ // Should match "Interstellar"
97
+ expect(result.current.data.some((doc) => doc._id === 'movie3')).toBe(true)
98
+ })
99
+
100
+ it('should apply ordering', () => {
101
+ const {result} = renderHook(() =>
102
+ usePaginatedList({
103
+ filter: '_type == "movie"',
104
+ orderings: [{field: 'releaseYear', direction: 'desc'}],
105
+ }),
106
+ )
107
+
108
+ // First item should be the most recent movie (Interstellar, 2014)
109
+ expect(result.current.data[0]._id).toBe('movie3')
110
+ })
111
+
112
+ it('should calculate pagination values correctly', () => {
113
+ const pageSize = 2
114
+ const {result} = renderHook(() => usePaginatedList({pageSize}))
115
+
116
+ expect(result.current.currentPage).toBe(1)
117
+ expect(result.current.totalPages).toBe(3) // 6 items with page size 2
118
+ expect(result.current.startIndex).toBe(0)
119
+ expect(result.current.endIndex).toBe(2)
120
+ expect(result.current.count).toBe(6)
121
+ })
122
+
123
+ it('should navigate to next page', () => {
124
+ const pageSize = 2
125
+ const {result} = renderHook(() => usePaginatedList({pageSize}))
126
+
127
+ expect(result.current.currentPage).toBe(1)
128
+ expect(result.current.data.length).toBe(pageSize)
129
+
130
+ act(() => {
131
+ result.current.nextPage()
132
+ })
133
+
134
+ expect(result.current.currentPage).toBe(2)
135
+ expect(result.current.startIndex).toBe(pageSize)
136
+ expect(result.current.endIndex).toBe(pageSize * 2)
137
+ })
138
+
139
+ it('should navigate to previous page', () => {
140
+ const pageSize = 2
141
+ const {result} = renderHook(() => usePaginatedList({pageSize}))
142
+
143
+ // Go to page 2 first
144
+ act(() => {
145
+ result.current.nextPage()
146
+ })
147
+
148
+ expect(result.current.currentPage).toBe(2)
149
+
150
+ // Then go back to page 1
151
+ act(() => {
152
+ result.current.previousPage()
153
+ })
154
+
155
+ expect(result.current.currentPage).toBe(1)
156
+ expect(result.current.startIndex).toBe(0)
157
+ })
158
+
159
+ it('should navigate to first page', () => {
160
+ const pageSize = 2
161
+ const {result} = renderHook(() => usePaginatedList({pageSize}))
162
+
163
+ // Go to last page first
164
+ act(() => {
165
+ result.current.lastPage()
166
+ })
167
+
168
+ expect(result.current.currentPage).toBe(3) // Last page (3rd page)
169
+
170
+ // Then go back to first page
171
+ act(() => {
172
+ result.current.firstPage()
173
+ })
174
+
175
+ expect(result.current.currentPage).toBe(1)
176
+ expect(result.current.startIndex).toBe(0)
177
+ })
178
+
179
+ it('should navigate to last page', () => {
180
+ const pageSize = 2
181
+ const {result} = renderHook(() => usePaginatedList({pageSize}))
182
+
183
+ act(() => {
184
+ result.current.lastPage()
185
+ })
186
+
187
+ expect(result.current.currentPage).toBe(3) // Last page (3rd page)
188
+ expect(result.current.startIndex).toBe(4) // Index 4-5 for the last page
189
+ })
190
+
191
+ it('should navigate to specific page', () => {
192
+ const pageSize = 2
193
+ const {result} = renderHook(() => usePaginatedList({pageSize}))
194
+
195
+ act(() => {
196
+ result.current.goToPage(2) // Go to page 2
197
+ })
198
+
199
+ expect(result.current.currentPage).toBe(2)
200
+ expect(result.current.startIndex).toBe(2) // Index 2-3 for page 2
201
+
202
+ // Should not navigate to invalid page numbers
203
+ act(() => {
204
+ result.current.goToPage(0) // Invalid page
205
+ })
206
+
207
+ expect(result.current.currentPage).toBe(2) // Should remain on page 2
208
+
209
+ act(() => {
210
+ result.current.goToPage(10) // Invalid page
211
+ })
212
+
213
+ expect(result.current.currentPage).toBe(2) // Should remain on page 2
214
+ })
215
+
216
+ it('should set page availability flags correctly', () => {
217
+ const pageSize = 2
218
+ const {result} = renderHook(() => usePaginatedList({pageSize}))
219
+ // On first page
220
+ expect(result.current.hasFirstPage).toBe(false)
221
+ expect(result.current.hasPreviousPage).toBe(false)
222
+ expect(result.current.hasNextPage).toBe(true)
223
+ expect(result.current.hasLastPage).toBe(true)
224
+ // Go to middle page
225
+ act(() => {
226
+ result.current.nextPage()
227
+ })
228
+ expect(result.current.hasFirstPage).toBe(true)
229
+ expect(result.current.hasPreviousPage).toBe(true)
230
+ expect(result.current.hasNextPage).toBe(true)
231
+ expect(result.current.hasLastPage).toBe(true)
232
+ // Go to last page
233
+ act(() => {
234
+ result.current.lastPage()
235
+ })
236
+ expect(result.current.hasFirstPage).toBe(true)
237
+ expect(result.current.hasPreviousPage).toBe(true)
238
+ expect(result.current.hasNextPage).toBe(false)
239
+ expect(result.current.hasLastPage).toBe(false)
240
+ })
241
+
242
+ // New test case for resetting the current page when filter changes
243
+ it('should reset current page when filter changes', () => {
244
+ const {result, rerender} = renderHook((props) => usePaginatedList(props), {
245
+ initialProps: {pageSize: 2, filter: ''},
246
+ })
247
+ // Initially, current page should be 1
248
+ expect(result.current.currentPage).toBe(1)
249
+ // Navigate to next page
250
+ act(() => {
251
+ result.current.nextPage()
252
+ })
253
+ expect(result.current.currentPage).toBe(2)
254
+ // Now update filter, which should reset the page to the first page
255
+ rerender({pageSize: 2, filter: '_type == "movie"'})
256
+ expect(result.current.currentPage).toBe(1)
257
+ expect(result.current.startIndex).toBe(0)
258
+ })
259
+ })