@sanity/sdk-react 2.8.0 → 3.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +125 -63
  2. package/dist/index.d.ts +381 -571
  3. package/dist/index.js +435 -366
  4. package/dist/index.js.map +1 -1
  5. package/package.json +7 -9
  6. package/src/_exports/index.ts +4 -0
  7. package/src/_exports/sdk-react.ts +16 -0
  8. package/src/components/SDKProvider.test.tsx +23 -58
  9. package/src/components/SDKProvider.tsx +38 -30
  10. package/src/components/SanityApp.test.tsx +12 -68
  11. package/src/components/SanityApp.tsx +88 -65
  12. package/src/components/auth/AuthBoundary.test.tsx +8 -26
  13. package/src/components/auth/LoginError.tsx +5 -5
  14. package/src/config/handles.ts +53 -0
  15. package/src/context/ComlinkTokenRefresh.test.tsx +27 -10
  16. package/src/context/DefaultResourceContext.ts +10 -0
  17. package/src/context/PerspectiveContext.ts +12 -0
  18. package/src/context/ResourceProvider.test.tsx +99 -19
  19. package/src/context/ResourceProvider.tsx +103 -37
  20. package/src/context/ResourcesContext.tsx +7 -0
  21. package/src/context/SDKStudioContext.test.tsx +33 -28
  22. package/src/context/SDKStudioContext.ts +6 -0
  23. package/src/context/renderSanityApp.test.tsx +49 -151
  24. package/src/context/renderSanityApp.tsx +8 -12
  25. package/src/hooks/agent/agentActions.test.tsx +1 -1
  26. package/src/hooks/agent/agentActions.ts +56 -19
  27. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +8 -2
  28. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +32 -8
  29. package/src/hooks/client/useClient.test.tsx +4 -1
  30. package/src/hooks/client/useClient.ts +0 -1
  31. package/src/hooks/context/useDefaultResource.test.tsx +25 -0
  32. package/src/hooks/context/useDefaultResource.ts +30 -0
  33. package/src/hooks/context/useSanityInstance.test.tsx +2 -140
  34. package/src/hooks/context/useSanityInstance.ts +9 -53
  35. package/src/hooks/dashboard/useDispatchIntent.test.ts +24 -15
  36. package/src/hooks/dashboard/useDispatchIntent.ts +7 -7
  37. package/src/hooks/dashboard/useManageFavorite.test.tsx +34 -94
  38. package/src/hooks/dashboard/useManageFavorite.ts +16 -10
  39. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +7 -5
  40. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +6 -2
  41. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +2 -0
  42. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -1
  43. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +17 -38
  44. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +12 -19
  45. package/src/hooks/datasets/useDatasets.test.ts +8 -22
  46. package/src/hooks/datasets/useDatasets.ts +8 -16
  47. package/src/hooks/document/useApplyDocumentActions.test.ts +98 -52
  48. package/src/hooks/document/useApplyDocumentActions.ts +35 -37
  49. package/src/hooks/document/useDocument.test.tsx +8 -37
  50. package/src/hooks/document/useDocument.ts +78 -129
  51. package/src/hooks/document/useDocumentEvent.test.tsx +7 -19
  52. package/src/hooks/document/useDocumentEvent.ts +21 -19
  53. package/src/hooks/document/useDocumentPermissions.test.tsx +75 -84
  54. package/src/hooks/document/useDocumentPermissions.ts +41 -28
  55. package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -3
  56. package/src/hooks/document/useDocumentSyncStatus.ts +19 -14
  57. package/src/hooks/document/useEditDocument.test.tsx +28 -70
  58. package/src/hooks/document/useEditDocument.ts +29 -149
  59. package/src/hooks/documents/useDocuments.test.tsx +44 -64
  60. package/src/hooks/documents/useDocuments.ts +19 -25
  61. package/src/hooks/helpers/createCallbackHook.test.tsx +19 -13
  62. package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
  63. package/src/hooks/helpers/createStateSourceHook.tsx +2 -4
  64. package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +65 -0
  65. package/src/hooks/helpers/useNormalizedResourceOptions.ts +127 -0
  66. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +27 -34
  67. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +19 -20
  68. package/src/hooks/presence/usePresence.test.tsx +71 -9
  69. package/src/hooks/presence/usePresence.ts +28 -3
  70. package/src/hooks/preview/useDocumentPreview.test.tsx +85 -193
  71. package/src/hooks/preview/useDocumentPreview.tsx +42 -62
  72. package/src/hooks/projection/useDocumentProjection.test.tsx +9 -37
  73. package/src/hooks/projection/useDocumentProjection.ts +9 -82
  74. package/src/hooks/projects/useProject.test.ts +1 -2
  75. package/src/hooks/projects/useProject.ts +7 -8
  76. package/src/hooks/query/useQuery.test.tsx +5 -6
  77. package/src/hooks/query/useQuery.ts +12 -91
  78. package/src/hooks/releases/useActiveReleases.test.tsx +2 -2
  79. package/src/hooks/releases/useActiveReleases.ts +25 -13
  80. package/src/hooks/releases/usePerspective.test.tsx +9 -17
  81. package/src/hooks/releases/usePerspective.ts +29 -18
  82. package/src/hooks/users/useUser.test.tsx +9 -3
  83. package/src/hooks/users/useUser.ts +1 -1
  84. package/src/hooks/users/useUsers.test.tsx +5 -2
  85. package/src/hooks/users/useUsers.ts +1 -1
  86. package/src/context/SourcesContext.tsx +0 -7
  87. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -85
@@ -1,84 +1,44 @@
1
- import {
2
- type ActionsResult,
3
- type DocumentOptions,
4
- editDocument,
5
- getDocumentState,
6
- type JsonMatch,
7
- resolveDocument,
8
- } from '@sanity/sdk'
9
- import {type SanityDocument} from 'groq'
1
+ import {type ActionsResult, editDocument, getDocumentState, resolveDocument} from '@sanity/sdk'
10
2
  import {useCallback} from 'react'
11
3
 
4
+ import {type DocumentHandle} from '../../config/handles'
12
5
  import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
13
7
  import {useApplyDocumentActions} from './useApplyDocumentActions'
14
8
 
15
9
  const ignoredKeys = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']
16
10
 
17
11
  type Updater<TValue> = TValue | ((currentValue: TValue) => TValue)
18
12
 
19
- // Overload 1: No path, relies on Typegen
20
- /**
21
- * @public
22
- * Edit an entire document, relying on Typegen for the type.
23
- *
24
- * @param options - Document options including `documentId`, `documentType`, and optionally `projectId`/`dataset`.
25
- * @returns A stable function to update the document state. Accepts either the new document state or an updater function `(currentValue) => nextValue`.
26
- * Returns a promise resolving to the {@link ActionsResult}.
27
- */
28
- export function useEditDocument<
29
- TDocumentType extends string = string,
30
- TDataset extends string = string,
31
- TProjectId extends string = string,
32
- >(
33
- options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
34
- ): (
35
- nextValue: Updater<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>,
36
- ) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
37
-
38
- // Overload 2: Path provided, relies on Typegen
39
- /**
40
- * @public
41
- * Edit a specific path within a document, relying on Typegen for the type.
42
- *
43
- * @param options - Document options including `documentId`, `documentType`, `path`, and optionally `projectId`/`dataset`.
44
- * @returns A stable function to update the value at the specified path. Accepts either the new value or an updater function `(currentValue) => nextValue`.
45
- * Returns a promise resolving to the {@link ActionsResult}.
46
- */
47
- export function useEditDocument<
48
- TPath extends string = string,
49
- TDocumentType extends string = string,
50
- TDataset extends string = string,
51
- TProjectId extends string = string,
52
- >(
53
- options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
54
- ): (
55
- nextValue: Updater<JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath>>,
56
- ) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
13
+ /** React-layer edit document options: DocumentHandle with optional path */
14
+ type EditDocumentOptions<TPath extends string | undefined = undefined> = DocumentHandle & {
15
+ path?: TPath
16
+ }
57
17
 
58
- // Overload 3: Explicit type, no path
18
+ // Overload 1: Explicit type, no path
59
19
  /**
60
20
  * @public
61
21
  * Edit an entire document with an explicit type `TData`.
62
22
  *
63
- * @param options - Document options including `documentId` and optionally `projectId`/`dataset`.
23
+ * @param options - Document options including `documentId` and optionally `resource` or `resourceName`.
64
24
  * @returns A stable function to update the document state. Accepts either the new document state (`TData`) or an updater function `(currentValue: TData) => nextValue: TData`.
65
25
  * Returns a promise resolving to the {@link ActionsResult}.
66
26
  */
67
27
  export function useEditDocument<TData>(
68
- options: DocumentOptions<undefined>,
28
+ options: EditDocumentOptions<undefined>,
69
29
  ): (nextValue: Updater<TData>) => Promise<ActionsResult>
70
30
 
71
- // Overload 4: Explicit type, path provided
31
+ // Overload 2: Explicit type, path provided
72
32
  /**
73
33
  * @public
74
34
  * Edit a specific path within a document with an explicit type `TData`.
75
35
  *
76
- * @param options - Document options including `documentId`, `path`, and optionally `projectId`/`dataset`.
36
+ * @param options - Document options including `documentId`, `path`, and optionally `resource` or `resourceName`.
77
37
  * @returns A stable function to update the value at the specified path. Accepts either the new value (`TData`) or an updater function `(currentValue: TData) => nextValue: TData`.
78
38
  * Returns a promise resolving to the {@link ActionsResult}.
79
39
  */
80
40
  export function useEditDocument<TData>(
81
- options: DocumentOptions<string>,
41
+ options: EditDocumentOptions<string>,
82
42
  ): (nextValue: Updater<TData>) => Promise<ActionsResult>
83
43
 
84
44
  /**
@@ -94,11 +54,9 @@ export function useEditDocument<TData>(
94
54
  * - Integrating with the active {@link SanityInstance} context.
95
55
  * - Utilizing `useApplyDocumentActions` internally for optimistic updates and transaction handling.
96
56
  *
97
- * It offers several overloads for flexibility:
98
- * 1. **Typegen (Full Document):** Edit the entire document, inferring types from your schema.
99
- * 2. **Typegen (Specific Path):** Edit a specific field, inferring types.
100
- * 3. **Explicit Type (Full Document):** Edit the entire document with a manually specified type.
101
- * 4. **Explicit Type (Specific Path):** Edit a specific field with a manually specified type.
57
+ * It offers overloads for flexibility:
58
+ * 1. **Explicit Type (Full Document):** Edit the entire document with a manually specified type.
59
+ * 2. **Explicit Type (Specific Path):** Edit a specific field with a manually specified type.
102
60
  *
103
61
  * **LiveEdit Documents:**
104
62
  * For documents using {@link DocumentHandle.liveEdit | liveEdit mode} (set via `liveEdit: true` in the document handle), edits are applied directly to the published document without creating a draft.
@@ -106,87 +64,7 @@ export function useEditDocument<TData>(
106
64
  * This hook relies on the document state being loaded. If the document is not yet available
107
65
  * (e.g., during initial load), the component using this hook will suspend.
108
66
  *
109
- * @example Basic Usage (Typegen, Full Document)
110
- * ```tsx
111
- * import {useCallback} from 'react';
112
- * import {useEditDocument, useDocument, type DocumentHandle} from '@sanity/sdk-react'
113
- *
114
- * // Assume 'product' schema has a 'title' field (string)
115
- * interface ProductEditorProps {
116
- * productHandle: DocumentHandle<'product'> // Typegen infers 'product' type
117
- * }
118
- *
119
- * function ProductEditor({ productHandle }: ProductEditorProps) {
120
- * // Fetch the document to display its current state (optional)
121
- * const {data: product} = useDocument(productHandle);
122
- * // Get the edit function for the full document
123
- * const editProduct = useEditDocument(productHandle);
124
- *
125
- * // Use useCallback for stable event handlers
126
- * const handleTitleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
127
- * const newTitle = event.target.value;
128
- * // Use the functional updater for safe partial updates
129
- * editProduct(prev => ({
130
- * ...prev,
131
- * title: newTitle,
132
- * })).
133
- * }, [editProduct]);
134
- *
135
- * return (
136
- * <div>
137
- * <label>
138
- * Product Title:
139
- * <input
140
- * type="text"
141
- * value={product?.title ?? ''}
142
- * onChange={handleTitleChange}
143
- * />
144
- * </label>
145
- * </div>
146
- * );
147
- * }
148
- * ```
149
- *
150
- * @example Editing a Specific Path (Typegen)
151
- * ```tsx
152
- * import React, { useCallback } from 'react';
153
- * import {useEditDocument, useDocument, type DocumentHandle, type DocumentOptions} from '@sanity/sdk-react'
154
- *
155
- * // Assume 'product' schema has a 'price' field (number)
156
- * interface ProductPriceEditorProps {
157
- * productHandle: DocumentHandle<'product'>;
158
- * }
159
- *
160
- * function ProductPriceEditor({ productHandle }: ProductPriceEditorProps) {
161
- * // Construct DocumentOptions internally, combining the handle and a hardcoded path
162
- * const priceOptions {
163
- * ...productHandle,
164
- * path: 'price', // Hardcode the path to edit
165
- * };
166
- *
167
- * // Fetch the current price to display it
168
- * const {data: currentPrice} = useDocument(priceOptions);
169
- * // Get the edit function for the specific path 'price'
170
- * const editPrice = useEditDocument(priceOptions);
171
- *
172
- * const handleSetFixedPrice = useCallback(() => {
173
- * // Update the price directly to a hardcoded value
174
- * editPrice(99.99)
175
- * }, [editPrice]);
176
- *
177
- * return (
178
- * <div>
179
- * <p>Current Price: {currentPrice}</p>
180
- * <button onClick={handleSetFixedPrice}>
181
- * Set Price to $99.99
182
- * </button>
183
- * </div>
184
- * );
185
- * }
186
- *
187
- * ```
188
- *
189
- * @example Usage with Explicit Types (Full Document)
67
+ * @example Basic Usage with Explicit Types (Full Document)
190
68
  * ```tsx
191
69
  * import React, { useCallback } from 'react';
192
70
  * import {useEditDocument, useDocument, type DocumentHandle, type SanityDocument} from '@sanity/sdk-react'
@@ -262,20 +140,22 @@ export function useEditDocument<TData>(
262
140
  export function useEditDocument({
263
141
  path,
264
142
  ...doc
265
- }: DocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
266
- const instance = useSanityInstance(doc)
143
+ }: EditDocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
144
+ const instance = useSanityInstance()
145
+ const normalizedDoc = useNormalizedResourceOptions(doc)
146
+
267
147
  const apply = useApplyDocumentActions()
268
148
  const isDocumentReady = useCallback(
269
- () => getDocumentState(instance, doc).getCurrent() !== undefined,
270
- [instance, doc],
149
+ () => getDocumentState(instance, normalizedDoc).getCurrent() !== undefined,
150
+ [instance, normalizedDoc],
271
151
  )
272
- if (!isDocumentReady()) throw resolveDocument(instance, doc)
152
+ if (!isDocumentReady()) throw resolveDocument(instance, normalizedDoc)
273
153
 
274
154
  return (updater: Updater<unknown>) => {
275
155
  const currentPath = path
276
156
 
277
157
  if (currentPath) {
278
- const stateWithOptions = getDocumentState(instance, {...doc, path})
158
+ const stateWithOptions = getDocumentState(instance, {...normalizedDoc, path})
279
159
  const currentValue = stateWithOptions.getCurrent()
280
160
 
281
161
  const nextValue =
@@ -283,10 +163,10 @@ export function useEditDocument({
283
163
  ? (updater as (prev: typeof currentValue) => typeof currentValue)(currentValue)
284
164
  : updater
285
165
 
286
- return apply(editDocument(doc, {set: {[currentPath]: nextValue}}))
166
+ return apply(editDocument(normalizedDoc, {set: {[currentPath]: nextValue}}))
287
167
  }
288
168
 
289
- const fullDocState = getDocumentState(instance, {...doc, path})
169
+ const fullDocState = getDocumentState(instance, {...normalizedDoc, path})
290
170
  const current = fullDocState.getCurrent() as object | null | undefined
291
171
  const nextValue =
292
172
  typeof updater === 'function'
@@ -308,8 +188,8 @@ export function useEditDocument({
308
188
  )
309
189
  .map((key) =>
310
190
  key in nextValue
311
- ? editDocument(doc, {set: {[key]: (nextValue as Record<string, unknown>)[key]}})
312
- : editDocument(doc, {unset: [key]}),
191
+ ? editDocument(normalizedDoc, {set: {[key]: (nextValue as Record<string, unknown>)[key]}})
192
+ : editDocument(normalizedDoc, {unset: [key]}),
313
193
  )
314
194
 
315
195
  return apply(editActions)
@@ -1,8 +1,10 @@
1
- import {act, renderHook} from '@testing-library/react'
1
+ import {type DatasetResource} from '@sanity/sdk'
2
2
  import {evaluateSync, parse, toJS} from 'groq-js'
3
3
  import {describe, vi} from 'vitest'
4
4
 
5
+ import {act, renderHook} from '../../../test/test-utils'
5
6
  import {ResourceProvider} from '../../context/ResourceProvider'
7
+ import {ResourcesContext} from '../../context/ResourcesContext'
6
8
  import {useQuery} from '../query/useQuery'
7
9
  import {useDocuments} from './useDocuments'
8
10
 
@@ -77,25 +79,13 @@ describe('useDocuments', () => {
77
79
 
78
80
  it('should respect custom page size', () => {
79
81
  const customBatchSize = 2
80
- const {result} = renderHook(() => useDocuments({batchSize: customBatchSize}), {
81
- wrapper: ({children}) => (
82
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
83
- {children}
84
- </ResourceProvider>
85
- ),
86
- })
82
+ const {result} = renderHook(() => useDocuments({batchSize: customBatchSize}))
87
83
 
88
84
  expect(result.current.data.length).toBe(customBatchSize)
89
85
  })
90
86
 
91
87
  it('should filter by document type', () => {
92
- const {result} = renderHook(() => useDocuments({filter: '_type == "movie"'}), {
93
- wrapper: ({children}) => (
94
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
95
- {children}
96
- </ResourceProvider>
97
- ),
98
- })
88
+ const {result} = renderHook(() => useDocuments({filter: '_type == "movie"'}))
99
89
 
100
90
  expect(result.current.data.every((doc) => doc.documentType === 'movie')).toBe(true)
101
91
  expect(result.current.count).toBe(5) // 5 movies in the dataset
@@ -103,32 +93,18 @@ describe('useDocuments', () => {
103
93
 
104
94
  // groq-js doesn't support search filters yet
105
95
  it.skip('should apply search filter', () => {
106
- const {result} = renderHook(() => useDocuments({search: 'inter'}), {
107
- wrapper: ({children}) => (
108
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
109
- {children}
110
- </ResourceProvider>
111
- ),
112
- })
96
+ const {result} = renderHook(() => useDocuments({search: 'inter'}))
113
97
 
114
98
  // Should match "Interstellar"
115
99
  expect(result.current.data.some((doc) => doc.documentId === 'movie3')).toBe(true)
116
100
  })
117
101
 
118
102
  it('should apply ordering', () => {
119
- const {result} = renderHook(
120
- () =>
121
- useDocuments({
122
- filter: '_type == "movie"',
123
- orderings: [{field: 'releaseYear', direction: 'desc'}],
124
- }),
125
- {
126
- wrapper: ({children}) => (
127
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
128
- {children}
129
- </ResourceProvider>
130
- ),
131
- },
103
+ const {result} = renderHook(() =>
104
+ useDocuments({
105
+ filter: '_type == "movie"',
106
+ orderings: [{field: 'releaseYear', direction: 'desc'}],
107
+ }),
132
108
  )
133
109
 
134
110
  // First item should be the most recent movie (Interstellar, 2014)
@@ -137,13 +113,7 @@ describe('useDocuments', () => {
137
113
 
138
114
  it('should load more data when loadMore is called', () => {
139
115
  const batchSize = 2
140
- const {result} = renderHook(() => useDocuments({batchSize: batchSize}), {
141
- wrapper: ({children}) => (
142
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
143
- {children}
144
- </ResourceProvider>
145
- ),
146
- })
116
+ const {result} = renderHook(() => useDocuments({batchSize: batchSize}))
147
117
 
148
118
  expect(result.current.data.length).toBe(batchSize)
149
119
 
@@ -155,13 +125,7 @@ describe('useDocuments', () => {
155
125
  })
156
126
 
157
127
  it('should indicate when there is more data to load', () => {
158
- const {result} = renderHook(() => useDocuments({batchSize: 3}), {
159
- wrapper: ({children}) => (
160
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
161
- {children}
162
- </ResourceProvider>
163
- ),
164
- })
128
+ const {result} = renderHook(() => useDocuments({batchSize: 3}))
165
129
  expect(result.current.hasMore).toBe(true)
166
130
  // Load all remaining data
167
131
  act(() => {
@@ -174,11 +138,6 @@ describe('useDocuments', () => {
174
138
  it('should reset limit when filter changes', () => {
175
139
  const {result, rerender} = renderHook((props) => useDocuments(props), {
176
140
  initialProps: {batchSize: 2, filter: ''},
177
- wrapper: ({children}) => (
178
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
179
- {children}
180
- </ResourceProvider>
181
- ),
182
141
  })
183
142
  // Initially, data length equals pageSize (2)
184
143
  expect(result.current.data.length).toBe(2)
@@ -194,23 +153,44 @@ describe('useDocuments', () => {
194
153
  expect(result.current.data.length).toBe(2)
195
154
  })
196
155
 
197
- it('should add projectId and dataset to document handles', () => {
198
- const {result} = renderHook(() => useDocuments({}), {
156
+ it('should add resources to document handles', () => {
157
+ const {result} = renderHook(() => useDocuments({}))
158
+
159
+ // Check that the first document handle has the projectId and dataset
160
+ expect((result.current.data[0].resource as DatasetResource).projectId).toBe('test')
161
+ expect((result.current.data[0].resource as DatasetResource).dataset).toBe('test')
162
+
163
+ // Verify all document handles have these properties
164
+ expect(
165
+ result.current.data.every(
166
+ (doc) =>
167
+ (doc.resource as DatasetResource).projectId === 'test' &&
168
+ (doc.resource as DatasetResource).dataset === 'test',
169
+ ),
170
+ ).toBe(true)
171
+ })
172
+
173
+ it('should resolve resourceName to the named dataset resource', () => {
174
+ const resources = {
175
+ default: {projectId: 'test-project', dataset: 'test-dataset'},
176
+ secondary: {projectId: 'secondary-project', dataset: 'secondary-dataset'},
177
+ }
178
+
179
+ const {result} = renderHook(() => useDocuments({resourceName: 'secondary'}), {
199
180
  wrapper: ({children}) => (
200
- <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
201
- {children}
181
+ <ResourceProvider resource={resources.default} fallback={null}>
182
+ <ResourcesContext.Provider value={resources}>{children}</ResourcesContext.Provider>
202
183
  </ResourceProvider>
203
184
  ),
204
185
  })
205
186
 
206
- // Check that the first document handle has the projectId and dataset
207
- expect(result.current.data[0].projectId).toBe('test-project')
208
- expect(result.current.data[0].dataset).toBe('test-dataset')
209
-
210
- // Verify all document handles have these properties
187
+ expect((result.current.data[0].resource as DatasetResource).projectId).toBe('secondary-project')
188
+ expect((result.current.data[0].resource as DatasetResource).dataset).toBe('secondary-dataset')
211
189
  expect(
212
190
  result.current.data.every(
213
- (doc) => doc.projectId === 'test-project' && doc.dataset === 'test-dataset',
191
+ (doc) =>
192
+ (doc.resource as DatasetResource).projectId === 'secondary-project' &&
193
+ (doc.resource as DatasetResource).dataset === 'secondary-dataset',
214
194
  ),
215
195
  ).toBe(true)
216
196
  })
@@ -1,14 +1,10 @@
1
- import {
2
- createGroqSearchFilter,
3
- type DatasetHandle,
4
- type DocumentHandle,
5
- type QueryOptions,
6
- } from '@sanity/sdk'
1
+ import {createGroqSearchFilter, type QueryOptions} from '@sanity/sdk'
7
2
  import {type SortOrderingItem} from '@sanity/types'
8
3
  import {pick} from 'lodash-es'
9
4
  import {useCallback, useEffect, useMemo, useState} from 'react'
10
5
 
11
- import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {type DocumentHandle, type ResourceHandle} from '../../config/handles'
7
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
12
8
  import {useQuery} from '../query/useQuery'
13
9
 
14
10
  const DEFAULT_BATCH_SIZE = 25
@@ -24,7 +20,9 @@ export interface DocumentsOptions<
24
20
  TDataset extends string = string,
25
21
  TProjectId extends string = string,
26
22
  >
27
- extends DatasetHandle<TDataset, TProjectId>, Pick<QueryOptions, 'perspective' | 'params'> {
23
+ extends
24
+ ResourceHandle<TProjectId, TDataset>,
25
+ Pick<QueryOptions<TDocumentType, TDataset, TProjectId>, 'params'> {
28
26
  /**
29
27
  * Filter documents by their `_type`. Can be a single type or an array of types.
30
28
  */
@@ -92,18 +90,16 @@ export interface DocumentsResponse<
92
90
  * @returns An object containing the list of document handles, the loading state, the total count of retrieved document handles, and a function to load more
93
91
  *
94
92
  * @remarks
95
- * - The returned document handles include projectId and dataset information from the current Sanity instance
93
+ * - The returned document handles include resource information from the current Sanity instance
96
94
  * - This makes them ready to use with document operations and other document hooks
97
- * - The hook automatically uses the correct Sanity instance based on the projectId and dataset in the options
95
+ * - The hook automatically uses the correct Sanity instance based on the resource in the options
98
96
  *
99
97
  * @example Basic infinite list with loading more
100
98
  * ```tsx
101
99
  * import {
102
100
  * useDocuments,
103
- * createDatasetHandle,
104
- * type DatasetHandle,
105
101
  * type DocumentHandle,
106
- * type SortOrderingItem
102
+ * type DocumentResource,
107
103
  * } from '@sanity/sdk-react'
108
104
  * import {Suspense} from 'react'
109
105
  *
@@ -119,14 +115,14 @@ export interface DocumentsResponse<
119
115
  *
120
116
  * // Define props for the list component
121
117
  * interface DocumentListProps {
122
- * dataset: DatasetHandle
118
+ * resource: DocumentResource
123
119
  * documentType: string
124
120
  * search?: string
125
121
  * }
126
122
  *
127
- * function DocumentList({dataset, documentType, search}: DocumentListProps) {
123
+ * function DocumentList({resource, documentType, search}: DocumentListProps) {
128
124
  * const { data, hasMore, isPending, loadMore, count } = useDocuments({
129
- * ...dataset,
125
+ * resource,
130
126
  * documentType,
131
127
  * search,
132
128
  * batchSize: 10,
@@ -140,7 +136,7 @@ export interface DocumentsResponse<
140
136
  * {data.map((docHandle) => (
141
137
  * <li key={docHandle.documentId}>
142
138
  * <Suspense fallback="Loading…">
143
- * <MyDocumentComponent docHandle={docHandle} />
139
+ * <MyDocumentComponent doc={docHandle} />
144
140
  * </Suspense>
145
141
  * </li>
146
142
  * ))}
@@ -155,8 +151,7 @@ export interface DocumentsResponse<
155
151
  * }
156
152
  *
157
153
  * // Usage:
158
- * // const myDatasetHandle = createDatasetHandle({ projectId: 'p1', dataset: 'production' })
159
- * // <DocumentList dataset={myDatasetHandle} documentType="post" search="Sanity" />
154
+ * // <DocumentList resource={{projectId: 'p1', dataset: 'production'}} documentType="post" search="Sanity" />
160
155
  * ```
161
156
  *
162
157
  * @example Using `filter` and `params` options for narrowing a collection
@@ -201,13 +196,14 @@ export function useDocuments<
201
196
  filter,
202
197
  orderings,
203
198
  documentType,
204
- ...options
199
+ ...rawOptions
205
200
  }: DocumentsOptions<TDocumentType, TDataset, TProjectId>): DocumentsResponse<
206
201
  TDocumentType,
207
202
  TDataset,
208
203
  TProjectId
209
204
  > {
210
- const instance = useSanityInstance(options)
205
+ const options =
206
+ useNormalizedResourceOptions<DocumentsOptions<TDocumentType, TDataset, TProjectId>>(rawOptions)
211
207
  const [limit, setLimit] = useState(batchSize)
212
208
  const documentTypes = useMemo(
213
209
  () =>
@@ -279,10 +275,8 @@ export function useDocuments<
279
275
  query: `{"count":${countQuery},"data":${dataQuery}}`,
280
276
  params: {
281
277
  ...params,
282
- __handle: {
283
- ...pick(instance.config, 'projectId', 'dataset', 'perspective'),
284
- ...pick(options, 'projectId', 'dataset', 'perspective'),
285
- },
278
+ // these are passed back to the user as part of each document handle
279
+ __handle: pick(options, ['resource', 'perspective']),
286
280
  __types: documentTypes,
287
281
  },
288
282
  })
@@ -1,7 +1,7 @@
1
1
  import {type SanityInstance} from '@sanity/sdk'
2
- import {renderHook} from '@testing-library/react'
3
2
  import {describe, expect, it, vi} from 'vitest'
4
3
 
4
+ import {renderHook} from '../../../test/test-utils'
5
5
  import {ResourceProvider} from '../../context/ResourceProvider'
6
6
  import {createCallbackHook} from './createCallbackHook'
7
7
 
@@ -21,13 +21,7 @@ describe('createCallbackHook', () => {
21
21
  const useTestHook = createCallbackHook(testCallback)
22
22
 
23
23
  // Render the hook
24
- const {result, rerender} = renderHook(() => useTestHook(), {
25
- wrapper: ({children}) => (
26
- <ResourceProvider projectId="p" dataset="d" fallback={null}>
27
- {children}
28
- </ResourceProvider>
29
- ),
30
- })
24
+ const {result, rerender} = renderHook(() => useTestHook())
31
25
 
32
26
  // Test the callback with parameters
33
27
  const result1 = result.current('test', 123)
@@ -44,13 +38,17 @@ describe('createCallbackHook', () => {
44
38
 
45
39
  it('should create new callback when instance changes', () => {
46
40
  // Create a test callback
47
- const testCallback = (instance: SanityInstance) => instance.config.projectId
41
+ const testCallback = (instance: SanityInstance) => instance.config.studio?.projectId
48
42
 
49
43
  // Create and render our hook with first provider
50
44
  const useTestHook = createCallbackHook(testCallback)
51
45
  const {result, unmount} = renderHook(() => useTestHook(), {
52
46
  wrapper: ({children}) => (
53
- <ResourceProvider projectId="p1" dataset="d" fallback={null}>
47
+ <ResourceProvider
48
+ studio={{projectId: 'p1'}}
49
+ resource={{projectId: 'p1', dataset: 'd'}}
50
+ fallback={null}
51
+ >
54
52
  {children}
55
53
  </ResourceProvider>
56
54
  ),
@@ -66,7 +64,11 @@ describe('createCallbackHook', () => {
66
64
  // Re-render with different provider configuration
67
65
  const {result: result2} = renderHook(() => useTestHook(), {
68
66
  wrapper: ({children}) => (
69
- <ResourceProvider projectId="p2" dataset="d" fallback={null}>
67
+ <ResourceProvider
68
+ studio={{projectId: 'p2'}}
69
+ resource={{projectId: 'p2', dataset: 'd'}}
70
+ fallback={null}
71
+ >
70
72
  {children}
71
73
  </ResourceProvider>
72
74
  ),
@@ -85,7 +87,7 @@ describe('createCallbackHook', () => {
85
87
  method: string,
86
88
  data: object,
87
89
  ) => ({
88
- url: `${instance.config.projectId}${path}`,
90
+ url: `${instance.config.studio?.projectId}${path}`,
89
91
  method,
90
92
  data,
91
93
  })
@@ -93,7 +95,11 @@ describe('createCallbackHook', () => {
93
95
  const useTestHook = createCallbackHook(testCallback)
94
96
  const {result} = renderHook(() => useTestHook(), {
95
97
  wrapper: ({children}) => (
96
- <ResourceProvider projectId="p" dataset="d" fallback={null}>
98
+ <ResourceProvider
99
+ studio={{projectId: 'p'}}
100
+ resource={{projectId: 'p', dataset: 'd'}}
101
+ fallback={null}
102
+ >
97
103
  {children}
98
104
  </ResourceProvider>
99
105
  ),