@sanity/sdk-react 0.0.0-alpha.29 → 0.0.0-alpha.30

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 (44) hide show
  1. package/README.md +5 -57
  2. package/dist/index.d.ts +998 -437
  3. package/dist/index.js +325 -259
  4. package/dist/index.js.map +1 -1
  5. package/package.json +13 -12
  6. package/src/_exports/sdk-react.ts +4 -1
  7. package/src/components/SDKProvider.tsx +6 -1
  8. package/src/components/SanityApp.test.tsx +29 -47
  9. package/src/components/SanityApp.tsx +12 -11
  10. package/src/components/auth/AuthBoundary.test.tsx +177 -7
  11. package/src/components/auth/AuthBoundary.tsx +31 -1
  12. package/src/components/auth/ConfigurationError.ts +22 -0
  13. package/src/components/auth/LoginError.tsx +9 -3
  14. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
  15. package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
  16. package/src/hooks/client/useClient.ts +3 -3
  17. package/src/hooks/comlink/useManageFavorite.test.ts +276 -27
  18. package/src/hooks/comlink/useManageFavorite.ts +102 -51
  19. package/src/hooks/comlink/useWindowConnection.ts +3 -2
  20. package/src/hooks/datasets/useDatasets.test.ts +80 -0
  21. package/src/hooks/datasets/useDatasets.ts +2 -1
  22. package/src/hooks/document/useApplyDocumentActions.ts +105 -31
  23. package/src/hooks/document/useDocument.test.ts +41 -4
  24. package/src/hooks/document/useDocument.ts +198 -114
  25. package/src/hooks/document/useDocumentEvent.test.ts +5 -5
  26. package/src/hooks/document/useDocumentEvent.ts +67 -23
  27. package/src/hooks/document/useDocumentPermissions.ts +47 -8
  28. package/src/hooks/document/useDocumentSyncStatus.test.ts +12 -5
  29. package/src/hooks/document/useDocumentSyncStatus.ts +41 -14
  30. package/src/hooks/document/useEditDocument.test.ts +24 -6
  31. package/src/hooks/document/useEditDocument.ts +238 -133
  32. package/src/hooks/documents/useDocuments.test.tsx +1 -1
  33. package/src/hooks/documents/useDocuments.ts +153 -44
  34. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +1 -1
  35. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +120 -47
  36. package/src/hooks/projection/useProjection.ts +134 -46
  37. package/src/hooks/projects/useProject.test.ts +80 -0
  38. package/src/hooks/projects/useProjects.test.ts +77 -0
  39. package/src/hooks/query/useQuery.test.tsx +4 -4
  40. package/src/hooks/query/useQuery.ts +115 -43
  41. package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
  42. package/src/hooks/releases/useActiveReleases.ts +39 -0
  43. package/src/hooks/releases/usePerspective.test.tsx +120 -0
  44. package/src/hooks/releases/usePerspective.ts +49 -0
@@ -1,4 +1,9 @@
1
- import {createGroqSearchFilter, type DocumentHandle, type QueryOptions} from '@sanity/sdk'
1
+ import {
2
+ createGroqSearchFilter,
3
+ type DatasetHandle,
4
+ type DocumentHandle,
5
+ type QueryOptions,
6
+ } from '@sanity/sdk'
2
7
  import {type SortOrderingItem} from '@sanity/types'
3
8
  import {pick} from 'lodash-es'
4
9
  import {useCallback, useEffect, useMemo, useState} from 'react'
@@ -7,16 +12,6 @@ import {useSanityInstance} from '../context/useSanityInstance'
7
12
  import {useQuery} from '../query/useQuery'
8
13
 
9
14
  const DEFAULT_BATCH_SIZE = 25
10
- const DEFAULT_PERSPECTIVE = 'drafts'
11
-
12
- /**
13
- * Result structure returned from the infinite list query
14
- * @internal
15
- */
16
- interface UseDocumentsQueryResult {
17
- count: number
18
- data: DocumentHandle[]
19
- }
20
15
 
21
16
  /**
22
17
  * Configuration options for the useDocuments hook
@@ -24,7 +19,16 @@ interface UseDocumentsQueryResult {
24
19
  * @beta
25
20
  * @category Types
26
21
  */
27
- export interface DocumentsOptions extends QueryOptions {
22
+ export interface DocumentsOptions<
23
+ TDocumentType extends string = string,
24
+ TDataset extends string = string,
25
+ TProjectId extends string = string,
26
+ > extends DatasetHandle<TDataset, TProjectId>,
27
+ Pick<QueryOptions, 'perspective' | 'params'> {
28
+ /**
29
+ * Filter documents by their `_type`. Can be a single type or an array of types.
30
+ */
31
+ documentType?: TDocumentType | TDocumentType[]
28
32
  /**
29
33
  * GROQ filter expression to apply to the query
30
34
  */
@@ -49,11 +53,15 @@ export interface DocumentsOptions extends QueryOptions {
49
53
  * @beta
50
54
  * @category Types
51
55
  */
52
- export interface DocumentsResponse {
56
+ export interface DocumentsResponse<
57
+ TDocumentType extends string = string,
58
+ TDataset extends string = string,
59
+ TProjectId extends string = string,
60
+ > {
53
61
  /**
54
62
  * Array of document handles for the current batch
55
63
  */
56
- data: DocumentHandle[]
64
+ data: DocumentHandle<TDocumentType, TDataset, TProjectId>[]
57
65
  /**
58
66
  * Whether there are more items available to load
59
67
  */
@@ -89,45 +97,136 @@ export interface DocumentsResponse {
89
97
  *
90
98
  * @example Basic infinite list with loading more
91
99
  * ```tsx
92
- * const { data, hasMore, isPending, loadMore, count } = useDocuments({
93
- * filter: '_type == "post"',
94
- * search: searchTerm,
95
- * batchSize: 10,
96
- * orderings: [{field: '_createdAt', direction: 'desc'}]
97
- * })
100
+ * import {
101
+ * useDocuments,
102
+ * createDatasetHandle,
103
+ * type DatasetHandle,
104
+ * type DocumentHandle,
105
+ * type SortOrderingItem
106
+ * } from '@sanity/sdk-react'
107
+ * import {Suspense} from 'react'
108
+ *
109
+ * // Define a component to display a single document (using useProjection for efficiency)
110
+ * function MyDocumentComponent({doc}: {doc: DocumentHandle}) {
111
+ * const {data} = useProjection<{title?: string}>({
112
+ * ...doc, // Pass the full handle
113
+ * projection: '{title}'
114
+ * })
115
+ *
116
+ * return <>{data?.title || 'Untitled'}</>
117
+ * }
118
+ *
119
+ * // Define props for the list component
120
+ * interface DocumentListProps {
121
+ * dataset: DatasetHandle
122
+ * documentType: string
123
+ * search?: string
124
+ * }
125
+ *
126
+ * function DocumentList({dataset, documentType, search}: DocumentListProps) {
127
+ * const { data, hasMore, isPending, loadMore, count } = useDocuments({
128
+ * ...dataset,
129
+ * documentType,
130
+ * search,
131
+ * batchSize: 10,
132
+ * orderings: [{field: '_createdAt', direction: 'desc'}],
133
+ * })
134
+ *
135
+ * return (
136
+ * <div>
137
+ * <p>Total documents: {count}</p>
138
+ * <ol>
139
+ * {data.map((docHandle) => (
140
+ * <li key={docHandle.documentId}>
141
+ * <Suspense fallback="Loading…">
142
+ * <MyDocumentComponent docHandle={docHandle} />
143
+ * </Suspense>
144
+ * </li>
145
+ * ))}
146
+ * </ol>
147
+ * {hasMore && (
148
+ * <button onClick={loadMore}>
149
+ * {isPending ? 'Loading...' : 'Load More'}
150
+ * </button>
151
+ * )}
152
+ * </div>
153
+ * )
154
+ * }
155
+ *
156
+ * // Usage:
157
+ * // const myDatasetHandle = createDatasetHandle({ projectId: 'p1', dataset: 'production' })
158
+ * // <DocumentList dataset={myDatasetHandle} documentType="post" search="Sanity" />
159
+ * ```
160
+ *
161
+ * @example Using `filter` and `params` options for narrowing a collection
162
+ * ```tsx
163
+ * import {useState} from 'react'
164
+ * import {useDocuments} from '@sanity/sdk-react'
165
+ *
166
+ * export default function FilteredAuthors() {
167
+ * const [max, setMax] = useState(2)
168
+ * const {data} = useDocuments({
169
+ * documentType: 'author',
170
+ * filter: 'length(books) <= $max',
171
+ * params: {max},
172
+ * })
98
173
  *
99
- * return (
100
- * <div>
101
- * Total documents: {count}
102
- * <ol>
103
- * {data.map((doc) => (
104
- * <li key={doc.documentId}>
105
- * <MyDocumentComponent doc={doc} />
106
- * </li>
174
+ * return (
175
+ * <>
176
+ * <input
177
+ * id="maxBooks"
178
+ * type="number"
179
+ * value={max}
180
+ * onChange={e => setMax(e.currentTarget.value)}
181
+ * />
182
+ * {data.map(author => (
183
+ * <Suspense key={author.documentId}>
184
+ * <MyAuthorComponent documentHandle={author} />
185
+ * </Suspense>
107
186
  * ))}
108
- * </ol>
109
- * {hasMore && <button onClick={loadMore} disabled={isPending}>
110
- * {isPending ? 'Loading...' : 'Load More'}
111
- * </button>}
112
- * </div>
113
- * )
187
+ * </>
188
+ * )
189
+ * }
114
190
  * ```
115
191
  */
116
- export function useDocuments({
192
+ export function useDocuments<
193
+ TDocumentType extends string = string,
194
+ TDataset extends string = string,
195
+ TProjectId extends string = string,
196
+ >({
117
197
  batchSize = DEFAULT_BATCH_SIZE,
118
198
  params,
119
199
  search,
120
200
  filter,
121
201
  orderings,
202
+ documentType,
122
203
  ...options
123
- }: DocumentsOptions): DocumentsResponse {
204
+ }: DocumentsOptions<TDocumentType, TDataset, TProjectId>): DocumentsResponse<
205
+ TDocumentType,
206
+ TDataset,
207
+ TProjectId
208
+ > {
124
209
  const instance = useSanityInstance(options)
125
- const perspective = options.perspective ?? DEFAULT_PERSPECTIVE
126
210
  const [limit, setLimit] = useState(batchSize)
211
+ const documentTypes = useMemo(
212
+ () =>
213
+ (Array.isArray(documentType) ? documentType : [documentType]).filter(
214
+ (i): i is TDocumentType => typeof i === 'string',
215
+ ),
216
+ [documentType],
217
+ )
127
218
 
128
219
  // Reset the limit to the current batchSize whenever any query parameters
129
220
  // (filter, search, params, orderings) or batchSize changes
130
- const key = JSON.stringify({filter, search, params, orderings, batchSize})
221
+ const key = JSON.stringify({
222
+ filter,
223
+ search,
224
+ params,
225
+ orderings,
226
+ batchSize,
227
+ types: documentTypes,
228
+ ...options,
229
+ })
131
230
  useEffect(() => {
132
231
  setLimit(batchSize)
133
232
  }, [key, batchSize])
@@ -144,13 +243,18 @@ export function useDocuments({
144
243
  }
145
244
  }
146
245
 
246
+ // Add type filter if specified
247
+ if (documentTypes?.length) {
248
+ conditions.push(`(_type in $__types)`)
249
+ }
250
+
147
251
  // Add additional filter if specified
148
252
  if (filter) {
149
253
  conditions.push(`(${filter})`)
150
254
  }
151
255
 
152
256
  return conditions.length ? `[${conditions.join(' && ')}]` : ''
153
- }, [filter, search])
257
+ }, [filter, search, documentTypes])
154
258
 
155
259
  const orderClause = orderings
156
260
  ? `| order(${orderings
@@ -163,21 +267,26 @@ export function useDocuments({
163
267
  .join(',')})`
164
268
  : ''
165
269
 
166
- const dataQuery = `*${filterClause}${orderClause}[0...${limit}]{"documentId":_id,"documentType":_type,...$__dataset}`
270
+ const dataQuery = `*${filterClause}${orderClause}[0...${limit}]{"documentId":_id,"documentType":_type,...$__handle}`
167
271
  const countQuery = `count(*${filterClause})`
168
272
 
169
273
  const {
170
274
  data: {count, data},
171
275
  isPending,
172
- } = useQuery<UseDocumentsQueryResult>(`{"count":${countQuery},"data":${dataQuery}}`, {
276
+ } = useQuery<{count: number; data: DocumentHandle<TDocumentType, TDataset, TProjectId>[]}>({
173
277
  ...options,
278
+ query: `{"count":${countQuery},"data":${dataQuery}}`,
174
279
  params: {
175
280
  ...params,
176
- __dataset: pick(instance.config, 'projectId', 'dataset'),
281
+ __handle: {
282
+ ...pick(instance.config, 'projectId', 'dataset', 'perspective'),
283
+ ...pick(options, 'projectId', 'dataset', 'perspective'),
284
+ },
285
+ __types: documentTypes,
177
286
  },
178
- perspective,
179
287
  })
180
288
 
289
+ // Now use the correctly typed variables
181
290
  const hasMore = data.length < count
182
291
 
183
292
  const loadMore = useCallback(() => {
@@ -186,6 +295,6 @@ export function useDocuments({
186
295
 
187
296
  return useMemo(
188
297
  () => ({data, hasMore, count, isPending, loadMore}),
189
- [data, hasMore, count, isPending, loadMore],
298
+ [count, data, hasMore, isPending, loadMore],
190
299
  )
191
300
  }
@@ -72,7 +72,7 @@ describe('usePaginatedDocuments', () => {
72
72
  },
73
73
  ]
74
74
 
75
- vi.mocked(useQuery).mockImplementation((query, options) => {
75
+ vi.mocked(useQuery).mockImplementation(({query, ...options}) => {
76
76
  const result = evaluateSync(parse(query), {dataset, params: options?.params}).get()
77
77
  return {
78
78
  data: result,
@@ -6,15 +6,18 @@ import {useCallback, useEffect, useMemo, useState} from 'react'
6
6
  import {useSanityInstance} from '../context/useSanityInstance'
7
7
  import {useQuery} from '../query/useQuery'
8
8
 
9
- const DEFAULT_PERSPECTIVE = 'drafts'
10
-
11
9
  /**
12
10
  * Configuration options for the usePaginatedDocuments hook
13
11
  *
14
12
  * @beta
15
13
  * @category Types
16
14
  */
17
- export interface PaginatedDocumentsOptions extends QueryOptions {
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[]
18
21
  /**
19
22
  * GROQ filter expression to apply to the query
20
23
  */
@@ -39,11 +42,15 @@ export interface PaginatedDocumentsOptions extends QueryOptions {
39
42
  * @beta
40
43
  * @category Types
41
44
  */
42
- export interface PaginatedDocumentsResponse {
45
+ export interface PaginatedDocumentsResponse<
46
+ TDocumentType extends string = string,
47
+ TDataset extends string = string,
48
+ TProjectId extends string = string,
49
+ > {
43
50
  /**
44
51
  * Array of document handles for the current page
45
52
  */
46
- data: DocumentHandle[]
53
+ data: DocumentHandle<TDocumentType, TDataset, TProjectId>[]
47
54
  /**
48
55
  * Whether a query is currently in progress
49
56
  */
@@ -126,54 +133,110 @@ export interface PaginatedDocumentsResponse {
126
133
  * @beta
127
134
  * @category Documents
128
135
  * @param options - Configuration options for the paginated list
129
- * @returns An object containing the current page of document handles, the loading and pagination state, and navigation functions
136
+ * @returns An object containing the list of document handles, pagination details, and functions to navigate between pages
130
137
  *
131
138
  * @remarks
132
139
  * - The returned document handles include projectId and dataset information from the current Sanity instance
133
140
  * - This makes them ready to use with document operations and other document hooks
134
141
  * - The hook automatically uses the correct Sanity instance based on the projectId and dataset in the options
135
142
  *
136
- * @example Basic usage
143
+ * @example Paginated list of documents with navigation
137
144
  * ```tsx
138
- * const {
139
- * data,
140
- * isPending,
141
- * currentPage,
142
- * totalPages,
143
- * nextPage,
144
- * previousPage,
145
- * hasNextPage,
146
- * hasPreviousPage
147
- * } = usePaginatedDocuments({
148
- * filter: '_type == "post"',
149
- * search: searchTerm,
150
- * pageSize: 10,
151
- * orderings: [{field: '_createdAt', direction: 'desc'}]
152
- * })
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'
153
155
  *
154
- * return (
155
- * <>
156
- * <table>
157
- * {data.map(doc => (
158
- * <MyTableRowComponent key={doc.documentId} doc={doc} />
159
- * ))}
160
- * </table>
161
- * {hasPreviousPage && <button onClick={previousPage}>Previous</button>}
162
- * {currentPage} / {totalPages}
163
- * {hasNextPage && <button onClick={nextPage}>Next</button>}
164
- * </>
165
- * )
166
- * ```
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
+ * }
167
217
  *
218
+ * // Usage:
219
+ * // const myDatasetHandle = createDatasetHandle({ projectId: 'p1', dataset: 'production' })
220
+ * // <PaginatedDocumentList dataset={myDatasetHandle} documentType="post" />
221
+ * ```
168
222
  */
169
- export function usePaginatedDocuments({
223
+ export function usePaginatedDocuments<
224
+ TDocumentType extends string = string,
225
+ TDataset extends string = string,
226
+ TProjectId extends string = string,
227
+ >({
228
+ documentType,
170
229
  filter = '',
171
230
  pageSize = 25,
172
231
  params = {},
173
232
  orderings,
174
233
  search,
175
234
  ...options
176
- }: PaginatedDocumentsOptions): PaginatedDocumentsResponse {
235
+ }: PaginatedDocumentsOptions<TDocumentType, TDataset, TProjectId>): PaginatedDocumentsResponse<
236
+ TDocumentType,
237
+ TDataset,
238
+ TProjectId
239
+ > {
177
240
  const instance = useSanityInstance(options)
178
241
  const [pageIndex, setPageIndex] = useState(0)
179
242
  const key = JSON.stringify({filter, search, params, orderings, pageSize})
@@ -185,7 +248,9 @@ export function usePaginatedDocuments({
185
248
 
186
249
  const startIndex = pageIndex * pageSize
187
250
  const endIndex = (pageIndex + 1) * pageSize
188
- const perspective = options.perspective ?? DEFAULT_PERSPECTIVE
251
+ const documentTypes = (Array.isArray(documentType) ? documentType : [documentType]).filter(
252
+ (i) => typeof i === 'string',
253
+ )
189
254
 
190
255
  const filterClause = useMemo(() => {
191
256
  const conditions: string[] = []
@@ -199,13 +264,17 @@ export function usePaginatedDocuments({
199
264
  }
200
265
  }
201
266
 
267
+ if (documentTypes?.length) {
268
+ conditions.push(`(_type in $__types)`)
269
+ }
270
+
202
271
  // Add additional filter if specified
203
272
  if (filter) {
204
273
  conditions.push(`(${filter})`)
205
274
  }
206
275
 
207
276
  return conditions.length ? `[${conditions.join(' && ')}]` : ''
208
- }, [filter, search])
277
+ }, [filter, search, documentTypes?.length])
209
278
 
210
279
  const orderClause = orderings
211
280
  ? `| order(${orderings
@@ -218,20 +287,24 @@ export function usePaginatedDocuments({
218
287
  .join(',')})`
219
288
  : ''
220
289
 
221
- const dataQuery = `*${filterClause}${orderClause}[${startIndex}...${endIndex}]{"documentId":_id,"documentType":_type,...$__dataset}`
290
+ const dataQuery = `*${filterClause}${orderClause}[${startIndex}...${endIndex}]{"documentId":_id,"documentType":_type,...$__handle}`
222
291
  const countQuery = `count(*${filterClause})`
223
292
 
224
293
  const {
225
294
  data: {data, count},
226
295
  isPending,
227
- } = useQuery<{data: DocumentHandle[]; count: number}>(
228
- `{"data":${dataQuery},"count":${countQuery}}`,
229
- {
230
- ...options,
231
- perspective,
232
- params: {...params, __dataset: pick(instance.config, 'projectId', 'dataset')},
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
+ },
233
306
  },
234
- )
307
+ })
235
308
 
236
309
  const totalPages = Math.ceil(count / pageSize)
237
310
  const currentPage = pageIndex + 1