@sanity/sdk-react 2.10.0 → 2.11.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.
- package/dist/index.d.ts +257 -200
- package/dist/index.js +364 -253
- package/dist/index.js.map +1 -1
- package/package.json +6 -9
- package/src/_exports/index.ts +2 -0
- package/src/_exports/sdk-react.ts +4 -0
- package/src/components/SDKProvider.test.tsx +5 -12
- package/src/components/SDKProvider.tsx +26 -24
- package/src/config/handles.ts +55 -0
- package/src/constants.ts +5 -0
- package/src/context/DefaultResourceContext.ts +10 -0
- package/src/context/PerspectiveContext.ts +12 -0
- package/src/context/ResourceProvider.test.tsx +2 -2
- package/src/context/ResourceProvider.tsx +53 -49
- package/src/hooks/agent/agentActions.ts +55 -38
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +42 -111
- package/src/hooks/context/useSanityInstance.ts +28 -50
- package/src/hooks/dashboard/useDispatchIntent.test.ts +5 -1
- package/src/hooks/dashboard/useDispatchIntent.ts +3 -3
- package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +1 -5
- package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +42 -77
- package/src/hooks/document/useApplyDocumentActions.ts +28 -62
- package/src/hooks/document/useDocument.ts +3 -5
- package/src/hooks/document/useDocumentEvent.ts +4 -3
- package/src/hooks/document/useDocumentPermissions.test.tsx +58 -150
- package/src/hooks/document/useDocumentPermissions.ts +78 -55
- package/src/hooks/document/useEditDocument.test.tsx +25 -60
- package/src/hooks/document/useEditDocument.ts +1 -1
- package/src/hooks/documents/useDocuments.ts +13 -8
- package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +85 -47
- package/src/hooks/organizations/useOrganization.test-d.ts +53 -0
- package/src/hooks/organizations/useOrganization.test.ts +65 -0
- package/src/hooks/organizations/useOrganization.ts +40 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +55 -0
- package/src/hooks/organizations/useOrganizations.test.ts +85 -0
- package/src/hooks/organizations/useOrganizations.ts +45 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +23 -9
- package/src/hooks/presence/usePresence.ts +4 -11
- package/src/hooks/preview/useDocumentPreview.tsx +4 -7
- package/src/hooks/projection/useDocumentProjection.ts +5 -7
- package/src/hooks/projects/useProject.test-d.ts +49 -0
- package/src/hooks/projects/useProject.ts +33 -41
- package/src/hooks/projects/useProjects.test-d.ts +49 -0
- package/src/hooks/projects/useProjects.ts +17 -23
- package/src/hooks/query/useQuery.ts +1 -1
- package/src/hooks/releases/useActiveReleases.ts +6 -6
- package/src/hooks/releases/usePerspective.ts +7 -12
- package/src/hooks/users/useUser.ts +1 -1
- package/src/hooks/users/useUsers.ts +1 -1
|
@@ -7,10 +7,9 @@ import {
|
|
|
7
7
|
type StateSource,
|
|
8
8
|
} from '@sanity/sdk'
|
|
9
9
|
import {type SanityDocument} from '@sanity/types'
|
|
10
|
-
import {renderHook} from '@testing-library/react'
|
|
11
10
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
12
11
|
|
|
13
|
-
import {
|
|
12
|
+
import {renderHook} from '../../../test/test-utils'
|
|
14
13
|
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
15
14
|
import {useEditDocument} from './useEditDocument'
|
|
16
15
|
|
|
@@ -42,6 +41,11 @@ const docHandle = createDocumentHandle({
|
|
|
42
41
|
documentType: 'book',
|
|
43
42
|
})
|
|
44
43
|
|
|
44
|
+
const normalizedDoc = {
|
|
45
|
+
...docHandle,
|
|
46
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
// Define a single generic TestDocument type
|
|
46
50
|
interface Book extends SanityDocument {
|
|
47
51
|
_type: 'book'
|
|
@@ -76,16 +80,10 @@ describe('useEditDocument hook', () => {
|
|
|
76
80
|
const apply = vi.fn().mockResolvedValue({transactionId: 'tx1'})
|
|
77
81
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
78
82
|
|
|
79
|
-
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'})
|
|
80
|
-
wrapper: ({children}) => (
|
|
81
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
82
|
-
{children}
|
|
83
|
-
</ResourceProvider>
|
|
84
|
-
),
|
|
85
|
-
})
|
|
83
|
+
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
|
|
86
84
|
const promise = result.current('newValue')
|
|
87
|
-
expect(editDocument).toHaveBeenCalledWith(
|
|
88
|
-
expect(apply).toHaveBeenCalledWith(editDocument(
|
|
85
|
+
expect(editDocument).toHaveBeenCalledWith(normalizedDoc, {set: {foo: 'newValue'}})
|
|
86
|
+
expect(apply).toHaveBeenCalledWith(editDocument(normalizedDoc, {set: {foo: 'newValue'}}))
|
|
89
87
|
const actionsResult = await promise
|
|
90
88
|
expect(actionsResult).toEqual({transactionId: 'tx1'})
|
|
91
89
|
})
|
|
@@ -103,15 +101,9 @@ describe('useEditDocument hook', () => {
|
|
|
103
101
|
const apply = vi.fn().mockResolvedValue({transactionId: 'tx2'})
|
|
104
102
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
105
103
|
|
|
106
|
-
const {result} = renderHook(() => useEditDocument(docHandle)
|
|
107
|
-
wrapper: ({children}) => (
|
|
108
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
109
|
-
{children}
|
|
110
|
-
</ResourceProvider>
|
|
111
|
-
),
|
|
112
|
-
})
|
|
104
|
+
const {result} = renderHook(() => useEditDocument(docHandle))
|
|
113
105
|
const promise = result.current({...doc, foo: 'baz', extra: 'old', _id: 'doc1'})
|
|
114
|
-
expect(apply).toHaveBeenCalledWith([editDocument(
|
|
106
|
+
expect(apply).toHaveBeenCalledWith([editDocument(normalizedDoc, {set: {foo: 'baz'}})])
|
|
115
107
|
const actionsResult = await promise
|
|
116
108
|
expect(actionsResult).toEqual({transactionId: 'tx2'})
|
|
117
109
|
})
|
|
@@ -127,16 +119,10 @@ describe('useEditDocument hook', () => {
|
|
|
127
119
|
const apply = vi.fn().mockResolvedValue({transactionId: 'tx3'})
|
|
128
120
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
129
121
|
|
|
130
|
-
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'})
|
|
131
|
-
wrapper: ({children}) => (
|
|
132
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
133
|
-
{children}
|
|
134
|
-
</ResourceProvider>
|
|
135
|
-
),
|
|
136
|
-
})
|
|
122
|
+
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
|
|
137
123
|
const promise = result.current((prev: unknown) => `${prev}Updated`) // 'bar' becomes 'barUpdated'
|
|
138
|
-
expect(editDocument).toHaveBeenCalledWith(
|
|
139
|
-
expect(apply).toHaveBeenCalledWith(editDocument(
|
|
124
|
+
expect(editDocument).toHaveBeenCalledWith(normalizedDoc, {set: {foo: 'barUpdated'}})
|
|
125
|
+
expect(apply).toHaveBeenCalledWith(editDocument(normalizedDoc, {set: {foo: 'barUpdated'}}))
|
|
140
126
|
const actionsResult = await promise
|
|
141
127
|
expect(actionsResult).toEqual({transactionId: 'tx3'})
|
|
142
128
|
})
|
|
@@ -153,15 +139,9 @@ describe('useEditDocument hook', () => {
|
|
|
153
139
|
const apply = vi.fn().mockResolvedValue({transactionId: 'tx4'})
|
|
154
140
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
155
141
|
|
|
156
|
-
const {result} = renderHook(() => useEditDocument(docHandle)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
{children}
|
|
160
|
-
</ResourceProvider>
|
|
161
|
-
),
|
|
162
|
-
})
|
|
163
|
-
const promise = result.current((prevDoc) => ({...prevDoc, foo: 'baz'}))
|
|
164
|
-
expect(apply).toHaveBeenCalledWith([editDocument(docHandle, {set: {foo: 'baz'}})])
|
|
142
|
+
const {result} = renderHook(() => useEditDocument(docHandle))
|
|
143
|
+
const promise = result.current((prevDoc: Book) => ({...prevDoc, foo: 'baz'}))
|
|
144
|
+
expect(apply).toHaveBeenCalledWith([editDocument(normalizedDoc, {set: {foo: 'baz'}})])
|
|
165
145
|
const actionsResult = await promise
|
|
166
146
|
expect(actionsResult).toEqual({transactionId: 'tx4'})
|
|
167
147
|
})
|
|
@@ -177,13 +157,7 @@ describe('useEditDocument hook', () => {
|
|
|
177
157
|
const fakeApply = vi.fn()
|
|
178
158
|
vi.mocked(useApplyDocumentActions).mockReturnValue(fakeApply)
|
|
179
159
|
|
|
180
|
-
const {result} = renderHook(() => useEditDocument(docHandle)
|
|
181
|
-
wrapper: ({children}) => (
|
|
182
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
183
|
-
{children}
|
|
184
|
-
</ResourceProvider>
|
|
185
|
-
),
|
|
186
|
-
})
|
|
160
|
+
const {result} = renderHook(() => useEditDocument(docHandle))
|
|
187
161
|
expect(() => result.current('notAnObject' as unknown as Book)).toThrowError(
|
|
188
162
|
'No path was provided to `useEditDocument` and the value provided was not a document object.',
|
|
189
163
|
)
|
|
@@ -203,22 +177,13 @@ describe('useEditDocument hook', () => {
|
|
|
203
177
|
vi.mocked(resolveDocument).mockReturnValue(resolveDocPromise)
|
|
204
178
|
|
|
205
179
|
// Render the hook and capture the thrown promise.
|
|
206
|
-
const {result} = renderHook(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
wrapper: ({children}) => (
|
|
216
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
217
|
-
{children}
|
|
218
|
-
</ResourceProvider>
|
|
219
|
-
),
|
|
220
|
-
},
|
|
221
|
-
)
|
|
180
|
+
const {result} = renderHook(() => {
|
|
181
|
+
try {
|
|
182
|
+
return useEditDocument(docHandle)
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return e
|
|
185
|
+
}
|
|
186
|
+
})
|
|
222
187
|
|
|
223
188
|
// When the document is not ready, the hook throws the promise from resolveDocument.
|
|
224
189
|
expect(result.current).toBe(resolveDocPromise)
|
|
@@ -286,7 +286,7 @@ export function useEditDocument({
|
|
|
286
286
|
path,
|
|
287
287
|
...doc
|
|
288
288
|
}: DocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
|
|
289
|
-
const instance = useSanityInstance(
|
|
289
|
+
const instance = useSanityInstance()
|
|
290
290
|
trackHookUsage(instance, 'useEditDocument')
|
|
291
291
|
const normalizedDoc = useNormalizedResourceOptions(doc)
|
|
292
292
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createGroqSearchFilter,
|
|
3
|
-
type DatasetHandle,
|
|
4
3
|
type DocumentHandle,
|
|
4
|
+
isDatasetResource,
|
|
5
5
|
type QueryOptions,
|
|
6
6
|
} from '@sanity/sdk'
|
|
7
|
+
import {pickProperties} from '@sanity/sdk/_internal'
|
|
7
8
|
import {type SortOrderingItem} from '@sanity/types'
|
|
8
9
|
import {useCallback, useMemo, useState} from 'react'
|
|
9
10
|
|
|
10
|
-
import {
|
|
11
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
12
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
11
13
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
12
14
|
import {useQuery} from '../query/useQuery'
|
|
13
15
|
|
|
@@ -24,7 +26,7 @@ export interface DocumentsOptions<
|
|
|
24
26
|
TDataset extends string = string,
|
|
25
27
|
TProjectId extends string = string,
|
|
26
28
|
>
|
|
27
|
-
extends
|
|
29
|
+
extends ResourceHandle<TDataset, TProjectId>, Pick<QueryOptions, 'perspective' | 'params'> {
|
|
28
30
|
/**
|
|
29
31
|
* Filter documents by their `_type`. Can be a single type or an array of types.
|
|
30
32
|
*/
|
|
@@ -201,14 +203,14 @@ export function useDocuments<
|
|
|
201
203
|
filter,
|
|
202
204
|
orderings,
|
|
203
205
|
documentType,
|
|
204
|
-
...
|
|
206
|
+
...rawOptions
|
|
205
207
|
}: DocumentsOptions<TDocumentType, TDataset, TProjectId>): DocumentsResponse<
|
|
206
208
|
TDocumentType,
|
|
207
209
|
TDataset,
|
|
208
210
|
TProjectId
|
|
209
211
|
> {
|
|
210
212
|
useTrackHookUsage('useDocuments')
|
|
211
|
-
const
|
|
213
|
+
const options = useNormalizedResourceOptions(rawOptions)
|
|
212
214
|
const [limit, setLimit] = useState(batchSize)
|
|
213
215
|
const documentTypes = useMemo(
|
|
214
216
|
() =>
|
|
@@ -284,9 +286,12 @@ export function useDocuments<
|
|
|
284
286
|
...params,
|
|
285
287
|
// these are passed back to the user as part of each document handle
|
|
286
288
|
__handle: {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
// keep projectId/dataset for backward compat until v4; resource is added
|
|
290
|
+
// intentionally so that hook consumers can resolve the correct resource
|
|
291
|
+
...(options.resource && isDatasetResource(options.resource)
|
|
292
|
+
? pickProperties(options.resource, ['projectId', 'dataset'])
|
|
293
|
+
: {}),
|
|
294
|
+
...pickProperties(options, ['perspective', 'resource']),
|
|
290
295
|
},
|
|
291
296
|
__types: documentTypes,
|
|
292
297
|
},
|
|
@@ -19,11 +19,10 @@ export function createStateSourceHook<TParams extends unknown[], TState>(
|
|
|
19
19
|
options: StateSourceFactory<TParams, TState> | CreateStateSourceHookOptions<TParams, TState>,
|
|
20
20
|
): (...params: TParams) => TState {
|
|
21
21
|
const getState = typeof options === 'function' ? options : options.getState
|
|
22
|
-
const getConfig = 'getConfig' in options ? options.getConfig : undefined
|
|
23
22
|
const suspense = 'shouldSuspend' in options && 'suspender' in options ? options : undefined
|
|
24
23
|
|
|
25
24
|
function useHook(...params: TParams) {
|
|
26
|
-
const instance = useSanityInstance(
|
|
25
|
+
const instance = useSanityInstance()
|
|
27
26
|
|
|
28
27
|
if (suspense?.suspender && suspense?.shouldSuspend?.(instance, ...params)) {
|
|
29
28
|
throw suspense.suspender(instance, ...params)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {createSanityInstance, type DocumentHandle} from '@sanity/sdk'
|
|
2
|
+
import {type ReactNode} from 'react'
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {renderHook, resources} from '../../../test/test-utils'
|
|
6
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
7
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
8
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
9
|
+
import {useNormalizedResourceOptions} from './useNormalizedResourceOptions'
|
|
10
|
+
|
|
11
|
+
// Wrapper that sets ResourceContext via the `resource` prop (tier 3).
|
|
12
|
+
// Includes ResourcesContext so resourceName resolution also works in these tests.
|
|
13
|
+
function ResourceContextWrapper({
|
|
14
|
+
children,
|
|
15
|
+
resource,
|
|
16
|
+
}: {
|
|
17
|
+
children: ReactNode
|
|
18
|
+
resource: {projectId: string; dataset: string}
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<ResourceProvider resource={resource} fallback={null}>
|
|
22
|
+
<ResourcesContext.Provider value={resources}>{children}</ResourcesContext.Provider>
|
|
23
|
+
</ResourceProvider>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Wrapper that provides an instance with no projectId/dataset and no ResourceContext (tier 5).
|
|
28
|
+
const bareInstance = createSanityInstance({})
|
|
29
|
+
function NoResourceWrapper({children}: {children: ReactNode}) {
|
|
30
|
+
return (
|
|
31
|
+
<SanityInstanceContext.Provider value={bareInstance}>{children}</SanityInstanceContext.Provider>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('useNormalizedResourceOptions', () => {
|
|
36
|
+
describe('tier 1 — explicit options', () => {
|
|
37
|
+
it('uses an explicit dataset resource object', () => {
|
|
38
|
+
const {result} = renderHook(() =>
|
|
39
|
+
useNormalizedResourceOptions({resource: {projectId: 'explicit', dataset: 'explicit-ds'}}),
|
|
40
|
+
)
|
|
41
|
+
expect(result.current.resource).toEqual({projectId: 'explicit', dataset: 'explicit-ds'})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('uses an explicit media-library resource object', () => {
|
|
45
|
+
const {result} = renderHook(() =>
|
|
46
|
+
useNormalizedResourceOptions({resource: {mediaLibraryId: 'ml-123'}}),
|
|
47
|
+
)
|
|
48
|
+
expect(result.current.resource).toEqual({mediaLibraryId: 'ml-123'})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('uses an explicit canvas resource object', () => {
|
|
52
|
+
const {result} = renderHook(() =>
|
|
53
|
+
useNormalizedResourceOptions({resource: {canvasId: 'canvas-123'}}),
|
|
54
|
+
)
|
|
55
|
+
expect(result.current.resource).toEqual({canvasId: 'canvas-123'})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('resolves resourceName to a named dataset resource', () => {
|
|
59
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({resourceName: 'dataset'}))
|
|
60
|
+
expect(result.current.resource).toEqual({
|
|
61
|
+
projectId: 'resource-project-id',
|
|
62
|
+
dataset: 'resource-dataset',
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('resolves resourceName to a named media-library resource', () => {
|
|
67
|
+
const {result} = renderHook(() =>
|
|
68
|
+
useNormalizedResourceOptions({resourceName: 'media-library'}),
|
|
69
|
+
)
|
|
70
|
+
expect(result.current.resource).toEqual({mediaLibraryId: 'media-library-id'})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('resolves resourceName to a named canvas resource', () => {
|
|
74
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({resourceName: 'canvas'}))
|
|
75
|
+
expect(result.current.resource).toEqual({canvasId: 'canvas-id'})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('throws when resourceName is not registered', () => {
|
|
79
|
+
expect(() =>
|
|
80
|
+
renderHook(() => useNormalizedResourceOptions({resourceName: 'unknown'})),
|
|
81
|
+
).toThrow(/no resource named/i)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('throws when both resource and resourceName are provided', () => {
|
|
85
|
+
expect(() =>
|
|
86
|
+
renderHook(() =>
|
|
87
|
+
useNormalizedResourceOptions({
|
|
88
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
89
|
+
resourceName: 'dataset',
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
).toThrow()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('resolves deprecated `source` as `resource`', () => {
|
|
96
|
+
const {result} = renderHook(() =>
|
|
97
|
+
useNormalizedResourceOptions({source: {projectId: 'src', dataset: 'src-ds'}}),
|
|
98
|
+
)
|
|
99
|
+
expect(result.current.resource).toEqual({projectId: 'src', dataset: 'src-ds'})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('resolves deprecated `sourceName` as `resourceName`', () => {
|
|
103
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({sourceName: 'dataset'}))
|
|
104
|
+
expect(result.current.resource).toEqual({
|
|
105
|
+
projectId: 'resource-project-id',
|
|
106
|
+
dataset: 'resource-dataset',
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('tier 2 — bare projectId/dataset in options', () => {
|
|
112
|
+
it('synthesizes a resource from projectId + dataset', () => {
|
|
113
|
+
const {result} = renderHook(() =>
|
|
114
|
+
useNormalizedResourceOptions({projectId: 'opt', dataset: 'opt-ds'}),
|
|
115
|
+
)
|
|
116
|
+
expect(result.current.resource).toEqual({projectId: 'opt', dataset: 'opt-ds'})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('falls through to context when only projectId is provided (no dataset)', () => {
|
|
120
|
+
// Only projectId is not enough to synthesize — should fall back to context resource
|
|
121
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({projectId: 'opt'}))
|
|
122
|
+
// Default test-utils: ResourceProvider projectId="test" dataset="test" → tier-3 via config synthesis
|
|
123
|
+
expect(result.current.resource).toEqual({projectId: 'test', dataset: 'test'})
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('tier 3 — ResourceContext', () => {
|
|
128
|
+
it('uses ResourceContext set via ResourceProvider `resource` prop', () => {
|
|
129
|
+
const contextResource = {projectId: 'ctx-project', dataset: 'ctx-dataset'}
|
|
130
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
131
|
+
wrapper: ({children}) => (
|
|
132
|
+
<ResourceContextWrapper resource={contextResource}>{children}</ResourceContextWrapper>
|
|
133
|
+
),
|
|
134
|
+
})
|
|
135
|
+
expect(result.current.resource).toEqual(contextResource)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('uses ResourceContext synthesized from ResourceProvider projectId/dataset', () => {
|
|
139
|
+
// ResourceProvider with projectId/dataset (no explicit resource prop) synthesizes ResourceContext
|
|
140
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}))
|
|
141
|
+
// Default test-utils: ResourceProvider projectId="test" dataset="test"
|
|
142
|
+
expect(result.current.resource).toEqual({projectId: 'test', dataset: 'test'})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('explicit resource in options takes precedence over ResourceContext', () => {
|
|
146
|
+
const {result} = renderHook(
|
|
147
|
+
() =>
|
|
148
|
+
useNormalizedResourceOptions({resource: {projectId: 'explicit', dataset: 'explicit-ds'}}),
|
|
149
|
+
{
|
|
150
|
+
wrapper: ({children}) => (
|
|
151
|
+
<ResourceContextWrapper resource={{projectId: 'ctx-project', dataset: 'ctx-dataset'}}>
|
|
152
|
+
{children}
|
|
153
|
+
</ResourceContextWrapper>
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
expect(result.current.resource).toEqual({projectId: 'explicit', dataset: 'explicit-ds'})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('tier 4 — SanityInstance config fallback', () => {
|
|
162
|
+
it('falls back to instance projectId/dataset when ResourceContext is not set', () => {
|
|
163
|
+
// Bare SanityInstanceContext with config — no ResourceProvider, so no ResourceContext
|
|
164
|
+
const instanceWithConfig = createSanityInstance({projectId: 'inst', dataset: 'inst-ds'})
|
|
165
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
166
|
+
wrapper: ({children}) => (
|
|
167
|
+
<SanityInstanceContext.Provider value={instanceWithConfig}>
|
|
168
|
+
{children}
|
|
169
|
+
</SanityInstanceContext.Provider>
|
|
170
|
+
),
|
|
171
|
+
})
|
|
172
|
+
expect(result.current.resource).toEqual({projectId: 'inst', dataset: 'inst-ds'})
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('tier 5 — no resource available', () => {
|
|
177
|
+
it('returns no resource when neither options, context, nor instance config provide one', () => {
|
|
178
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
179
|
+
wrapper: NoResourceWrapper,
|
|
180
|
+
})
|
|
181
|
+
expect(result.current).not.toHaveProperty('resource')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('perspective resolution', () => {
|
|
186
|
+
it('uses explicit perspective from options', () => {
|
|
187
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({perspective: 'published'}))
|
|
188
|
+
expect(result.current.perspective).toBe('published')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('falls back to PerspectiveContext when no perspective in options', () => {
|
|
192
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
193
|
+
wrapper: ({children}) => (
|
|
194
|
+
<ResourceProvider perspective="previewDrafts" fallback={null}>
|
|
195
|
+
{children}
|
|
196
|
+
</ResourceProvider>
|
|
197
|
+
),
|
|
198
|
+
})
|
|
199
|
+
expect(result.current.perspective).toBe('previewDrafts')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('explicit perspective overrides PerspectiveContext', () => {
|
|
203
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({perspective: 'published'}), {
|
|
204
|
+
wrapper: ({children}) => (
|
|
205
|
+
<ResourceProvider perspective="previewDrafts" fallback={null}>
|
|
206
|
+
{children}
|
|
207
|
+
</ResourceProvider>
|
|
208
|
+
),
|
|
209
|
+
})
|
|
210
|
+
expect(result.current.perspective).toBe('published')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('omits perspective from result when not set', () => {
|
|
214
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
215
|
+
wrapper: NoResourceWrapper,
|
|
216
|
+
})
|
|
217
|
+
expect(result.current).not.toHaveProperty('perspective')
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('field stripping', () => {
|
|
222
|
+
it('strips resourceName from the result', () => {
|
|
223
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({resourceName: 'dataset'}))
|
|
224
|
+
expect(result.current).not.toHaveProperty('resourceName')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('strips projectId and dataset from the result when synthesized into resource', () => {
|
|
228
|
+
const {result} = renderHook(() =>
|
|
229
|
+
useNormalizedResourceOptions({projectId: 'p', dataset: 'd'}),
|
|
230
|
+
)
|
|
231
|
+
expect(result.current).not.toHaveProperty('projectId')
|
|
232
|
+
expect(result.current).not.toHaveProperty('dataset')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('strips deprecated source from the result', () => {
|
|
236
|
+
const {result} = renderHook(() =>
|
|
237
|
+
useNormalizedResourceOptions({source: {projectId: 'src', dataset: 'src-ds'}}),
|
|
238
|
+
)
|
|
239
|
+
expect(result.current).not.toHaveProperty('source')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('strips deprecated sourceName from the result', () => {
|
|
243
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({sourceName: 'dataset'}))
|
|
244
|
+
expect(result.current).not.toHaveProperty('sourceName')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('preserves unrelated fields', () => {
|
|
248
|
+
const opts: DocumentHandle = {documentId: 'doc-1', documentType: 'article'}
|
|
249
|
+
const {result} = renderHook(() => useNormalizedResourceOptions(opts))
|
|
250
|
+
expect(result.current).toMatchObject({documentId: 'doc-1', documentType: 'article'})
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
})
|