@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.1
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/README.md +103 -155
- package/dist/index.d.ts +1564 -682
- package/dist/index.js +1118 -612
- package/dist/index.js.map +1 -1
- package/package.json +51 -54
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +26 -17
- package/src/components/SDKProvider.test.tsx +173 -32
- package/src/components/SDKProvider.tsx +87 -28
- package/src/components/SanityApp.test.tsx +68 -12
- package/src/components/SanityApp.tsx +94 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +26 -8
- package/src/components/auth/AuthBoundary.tsx +28 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +103 -59
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/CorsErrorComponent.tsx +2 -2
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +29 -27
- package/src/constants.ts +5 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
- package/src/context/DefaultResourceContext.ts +3 -3
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/PerspectiveContext.ts +3 -3
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +72 -90
- package/src/context/ResourceProvider.tsx +59 -93
- package/src/context/SDKStudioContext.test.tsx +28 -33
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/renderSanityApp.test.tsx +151 -49
- package/src/context/renderSanityApp.tsx +12 -8
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +51 -71
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useCreateUserApplication.ts +11 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
- package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplication.ts +14 -0
- package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplications.ts +14 -0
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/client/useClient.test.tsx +1 -4
- package/src/hooks/client/useClient.ts +1 -0
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +72 -3
- package/src/hooks/context/useSanityInstance.ts +30 -8
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +10 -16
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +1 -2
- package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
- package/src/hooks/dashboard/useWindowTitle.ts +114 -0
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +33 -13
- package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
- package/src/hooks/document/useApplyDocumentActions.ts +66 -48
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.test.tsx +37 -8
- package/src/hooks/document/useDocument.ts +131 -66
- package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
- package/src/hooks/document/useDocumentEvent.ts +16 -10
- package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
- package/src/hooks/document/useDocumentPermissions.ts +71 -39
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
- package/src/hooks/document/useEditDocument.test.tsx +21 -14
- package/src/hooks/document/useEditDocument.ts +163 -17
- package/src/hooks/documents/useDocuments.test.tsx +64 -44
- package/src/hooks/documents/useDocuments.ts +36 -20
- package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
- package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +92 -50
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
- package/src/hooks/organizations/useOrganization.test.ts +63 -0
- package/src/hooks/organizations/useOrganization.ts +34 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
- package/src/hooks/organizations/useOrganizations.test.ts +63 -0
- package/src/hooks/organizations/useOrganizations.ts +39 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +40 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +9 -20
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +10 -8
- package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
- package/src/hooks/projection/useDocumentProjection.ts +81 -2
- package/src/hooks/projects/useProject.test-d.ts +56 -0
- package/src/hooks/projects/useProject.test.tsx +120 -0
- package/src/hooks/projects/useProject.ts +42 -43
- package/src/hooks/projects/useProjects.test-d.ts +60 -0
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +19 -26
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +91 -9
- package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
- package/src/hooks/releases/useActiveReleases.ts +28 -24
- package/src/hooks/releases/useAllReleases.test.tsx +93 -0
- package/src/hooks/releases/useAllReleases.ts +62 -0
- package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
- package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
- package/src/hooks/releases/usePerspective.test.tsx +18 -10
- package/src/hooks/releases/usePerspective.ts +13 -17
- package/src/hooks/users/useUser.test.tsx +3 -9
- package/src/hooks/users/useUser.ts +2 -0
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +37 -3
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/context/useDefaultResource.test.tsx +0 -25
- package/src/hooks/context/useDefaultResource.ts +0 -30
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
- package/src/hooks/datasets/useDatasets.test.ts +0 -66
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
- package/src/hooks/projects/useProject.test.ts +0 -79
|
@@ -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
|
+
})
|
|
@@ -1,44 +1,57 @@
|
|
|
1
1
|
import {type DocumentResource, type PerspectiveHandle} from '@sanity/sdk'
|
|
2
|
-
import {useContext} from 'react'
|
|
2
|
+
import {useContext, useMemo} from 'react'
|
|
3
3
|
|
|
4
4
|
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
5
5
|
import {PerspectiveContext} from '../../context/PerspectiveContext'
|
|
6
6
|
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
7
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
8
|
+
|
|
9
|
+
type NormalizedResourceFields = 'resourceName' | 'source' | 'sourceName' | 'projectId' | 'dataset'
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
|
-
* You should generally prefer to use the React-layer handle types (ResourceHandle, DocumentHandle) from '\@sanity/sdk-react' instead.
|
|
10
|
-
* This type is useful for non-handles (like document actions) that we still want to resolve resources for.
|
|
11
12
|
* Adds React hook support (resourceName resolution) to core types.
|
|
12
|
-
*
|
|
13
|
+
* Prefer using the React-layer handle types (ResourceHandle, DocumentHandle)
|
|
14
|
+
* from `@sanity/sdk-react` — this wrapper is kept for cases where overloads
|
|
15
|
+
* don't fit (e.g. non-handle options objects).
|
|
16
|
+
*
|
|
17
|
+
* @typeParam T - The core type to extend (must have optional `resource` field)
|
|
18
|
+
* @beta
|
|
13
19
|
*/
|
|
14
|
-
export type WithResourceNameSupport<T> =
|
|
15
|
-
resource?: DocumentResource
|
|
20
|
+
export type WithResourceNameSupport<T extends {resource?: DocumentResource}> = T & {
|
|
16
21
|
/**
|
|
17
22
|
* Optional name of a resource to resolve from context.
|
|
18
23
|
* If provided, will be resolved to a `DocumentResource` via `ResourcesContext`.
|
|
19
24
|
* @beta
|
|
20
25
|
*/
|
|
21
26
|
resourceName?: string
|
|
27
|
+
/**
|
|
28
|
+
* @deprecated Use `resourceName` instead.
|
|
29
|
+
* @beta
|
|
30
|
+
*/
|
|
31
|
+
sourceName?: string
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* Pure function that normalizes options by resolving `resourceName` to a `DocumentResource`
|
|
26
|
-
* using the provided resources map
|
|
27
|
-
*
|
|
28
|
-
* and you cannot call the {@link useNormalizedResourceOptions} hook.
|
|
36
|
+
* using the provided resources map. Use this when options are only available at call time
|
|
37
|
+
* (e.g. inside a callback) and you cannot call the {@link useNormalizedResourceOptions} hook.
|
|
29
38
|
*
|
|
30
39
|
* @typeParam T - The options type (must include optional resource field)
|
|
31
40
|
* @param options - Options that may include `resourceName` and/or `resource`
|
|
32
41
|
* @param resources - Map of resource names to DocumentResource (e.g. from ResourcesContext)
|
|
33
|
-
* @param contextResource - Resource from context (
|
|
34
|
-
* @param contextPerspective - Perspective from context (
|
|
35
|
-
* @returns Normalized options with `resourceName` removed and
|
|
42
|
+
* @param contextResource - Resource from context (from ResourceContext)
|
|
43
|
+
* @param contextPerspective - Perspective from context (from PerspectiveContext)
|
|
44
|
+
* @returns Normalized options with `resourceName` removed and `resource` resolved
|
|
36
45
|
* @internal
|
|
37
46
|
*/
|
|
38
47
|
export function normalizeResourceOptions<
|
|
39
48
|
T extends {
|
|
40
49
|
resource?: DocumentResource
|
|
41
50
|
resourceName?: string
|
|
51
|
+
source?: DocumentResource
|
|
52
|
+
sourceName?: string
|
|
53
|
+
projectId?: string
|
|
54
|
+
dataset?: string
|
|
42
55
|
perspective?: unknown
|
|
43
56
|
},
|
|
44
57
|
>(
|
|
@@ -46,70 +59,89 @@ export function normalizeResourceOptions<
|
|
|
46
59
|
resources: Record<string, DocumentResource>,
|
|
47
60
|
contextResource?: DocumentResource,
|
|
48
61
|
contextPerspective?: PerspectiveHandle['perspective'],
|
|
49
|
-
): Omit<T,
|
|
50
|
-
const {resourceName, ...rest} = options
|
|
62
|
+
): Omit<T, NormalizedResourceFields> {
|
|
63
|
+
const {resourceName, sourceName, source, projectId, dataset, ...rest} = options
|
|
64
|
+
|
|
65
|
+
// Coalesce deprecated aliases to their canonical equivalents
|
|
66
|
+
const effectiveResourceName = resourceName ?? sourceName
|
|
67
|
+
const effectiveResource = options.resource ?? source
|
|
51
68
|
|
|
52
|
-
if (
|
|
69
|
+
if (effectiveResourceName && effectiveResource) {
|
|
53
70
|
throw new Error(
|
|
54
|
-
`Resource name ${JSON.stringify(
|
|
71
|
+
`Resource name ${JSON.stringify(effectiveResourceName)} and resource ${JSON.stringify(effectiveResource)} cannot be used together.`,
|
|
55
72
|
)
|
|
56
73
|
}
|
|
57
74
|
|
|
58
|
-
let resolvedResource: DocumentResource | undefined
|
|
75
|
+
let resolvedResource: DocumentResource | undefined
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
77
|
+
// Tier (a): explicit resource object or resourceName lookup
|
|
78
|
+
if (effectiveResource) {
|
|
79
|
+
resolvedResource = effectiveResource
|
|
80
|
+
} else if (effectiveResourceName) {
|
|
81
|
+
if (!Object.hasOwn(resources, effectiveResourceName)) {
|
|
62
82
|
throw new Error(
|
|
63
|
-
`There's no resource named ${JSON.stringify(
|
|
64
|
-
'Register it via the resources prop on <SanityApp>.',
|
|
83
|
+
`There's no resource named ${JSON.stringify(effectiveResourceName)} in context. Please use <ResourceProvider>.`,
|
|
65
84
|
)
|
|
66
85
|
}
|
|
67
|
-
resolvedResource = resources[
|
|
86
|
+
resolvedResource = resources[effectiveResourceName]
|
|
68
87
|
}
|
|
69
88
|
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
// Tier (b): projectId or dataset in options → synthesize a resource
|
|
90
|
+
if (!resolvedResource && projectId && dataset) {
|
|
91
|
+
resolvedResource = {
|
|
92
|
+
projectId,
|
|
93
|
+
dataset,
|
|
94
|
+
}
|
|
72
95
|
}
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
97
|
+
// Tier (c): fall back to whatever ResourceContext provides
|
|
98
|
+
if (!resolvedResource) {
|
|
99
|
+
resolvedResource = contextResource
|
|
78
100
|
}
|
|
79
101
|
|
|
102
|
+
// Inject perspective from context when not explicitly provided in options
|
|
80
103
|
const resolvedPerspective = Object.hasOwn(options, 'perspective')
|
|
81
104
|
? options.perspective
|
|
82
105
|
: contextPerspective
|
|
83
106
|
|
|
84
107
|
return {
|
|
85
108
|
...rest,
|
|
86
|
-
resource: resolvedResource,
|
|
109
|
+
...(resolvedResource !== undefined && {resource: resolvedResource}),
|
|
87
110
|
...(resolvedPerspective !== undefined && {perspective: resolvedPerspective}),
|
|
88
|
-
}
|
|
111
|
+
}
|
|
89
112
|
}
|
|
90
113
|
|
|
91
114
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
115
|
+
* Returns the effective context resource: the `ResourceContext` value if set,
|
|
116
|
+
* otherwise a resource synthesized from the current `SanityInstance` config
|
|
117
|
+
* (tier-d fallback — returns `undefined` for studio-style configs with no project).
|
|
94
118
|
*
|
|
95
|
-
*
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export function useEffectiveContextResource(): DocumentResource | undefined {
|
|
122
|
+
const contextResource = useContext(ResourceContext)
|
|
123
|
+
const instance = useContext(SanityInstanceContext)
|
|
124
|
+
const {projectId, dataset} = instance?.config ?? {}
|
|
125
|
+
|
|
126
|
+
return useMemo(() => {
|
|
127
|
+
if (contextResource) return contextResource
|
|
128
|
+
if (projectId && dataset) return {projectId, dataset}
|
|
129
|
+
return undefined
|
|
130
|
+
}, [contextResource, projectId, dataset])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Normalizes hook options by resolving `resourceName` to a `DocumentResource`.
|
|
102
135
|
*
|
|
103
|
-
* @remarks
|
|
104
136
|
* Resolution priority for resource:
|
|
105
|
-
* 1.
|
|
106
|
-
* 2.
|
|
107
|
-
* 3.
|
|
108
|
-
* 4.
|
|
137
|
+
* 1. Explicit `resource` or `resourceName` in options
|
|
138
|
+
* 2. Bare `projectId`/`dataset` pair in options → synthesized into a resource
|
|
139
|
+
* 3. `ResourceContext` value (set by `ResourceProvider` / `SDKProvider`)
|
|
140
|
+
* 4. Current `SanityInstance` config — falls back to `undefined` for studio configs
|
|
109
141
|
*
|
|
110
142
|
* Resolution priority for perspective:
|
|
111
|
-
* 1.
|
|
112
|
-
* 2.
|
|
143
|
+
* 1. Explicit `perspective` in options
|
|
144
|
+
* 2. `PerspectiveContext` value (set by `ResourceProvider`)
|
|
113
145
|
*
|
|
114
146
|
* @internal
|
|
115
147
|
*/
|
|
@@ -117,11 +149,21 @@ export function useNormalizedResourceOptions<
|
|
|
117
149
|
T extends {
|
|
118
150
|
resource?: DocumentResource
|
|
119
151
|
resourceName?: string
|
|
120
|
-
|
|
152
|
+
source?: DocumentResource
|
|
153
|
+
sourceName?: string
|
|
154
|
+
projectId?: string
|
|
155
|
+
dataset?: string
|
|
156
|
+
perspective?: PerspectiveHandle['perspective']
|
|
121
157
|
},
|
|
122
|
-
>(
|
|
158
|
+
>(
|
|
159
|
+
options: T,
|
|
160
|
+
): Omit<T, NormalizedResourceFields> & {
|
|
161
|
+
resource?: DocumentResource
|
|
162
|
+
perspective?: PerspectiveHandle['perspective']
|
|
163
|
+
} {
|
|
123
164
|
const resources = useContext(ResourcesContext)
|
|
124
|
-
const
|
|
165
|
+
const effectiveContextResource = useEffectiveContextResource()
|
|
125
166
|
const contextPerspective = useContext(PerspectiveContext)
|
|
126
|
-
|
|
167
|
+
|
|
168
|
+
return normalizeResourceOptions(options, resources, effectiveContextResource, contextPerspective)
|
|
127
169
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {type DocumentResource} from '@sanity/sdk'
|
|
2
|
+
import {renderHook} from '@testing-library/react'
|
|
3
|
+
import {type ReactNode} from 'react'
|
|
4
|
+
import {describe, expect, it} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
7
|
+
import {ProjectContext} from '../../context/ProjectContext'
|
|
8
|
+
import {useResolvedProjectId} from './useResolvedProjectId'
|
|
9
|
+
|
|
10
|
+
const datasetResource: DocumentResource = {projectId: 'resource-project', dataset: 'production'}
|
|
11
|
+
|
|
12
|
+
describe('useResolvedProjectId', () => {
|
|
13
|
+
it('prefers an explicit projectId on the options', () => {
|
|
14
|
+
const {result} = renderHook(() => useResolvedProjectId({projectId: 'option-project'}), {
|
|
15
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
16
|
+
<ResourceContext.Provider value={datasetResource}>
|
|
17
|
+
<ProjectContext.Provider value="context-project">{children}</ProjectContext.Provider>
|
|
18
|
+
</ResourceContext.Provider>
|
|
19
|
+
),
|
|
20
|
+
})
|
|
21
|
+
expect(result.current).toBe('option-project')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('falls back to the ambient project scope (ProjectContext) over the resource', () => {
|
|
25
|
+
const {result} = renderHook(() => useResolvedProjectId(), {
|
|
26
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
27
|
+
<ResourceContext.Provider value={datasetResource}>
|
|
28
|
+
<ProjectContext.Provider value="context-project">{children}</ProjectContext.Provider>
|
|
29
|
+
</ResourceContext.Provider>
|
|
30
|
+
),
|
|
31
|
+
})
|
|
32
|
+
expect(result.current).toBe('context-project')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('falls back to the resolved resource projectId', () => {
|
|
36
|
+
const {result} = renderHook(() => useResolvedProjectId(), {
|
|
37
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
38
|
+
<ResourceContext.Provider value={datasetResource}>{children}</ResourceContext.Provider>
|
|
39
|
+
),
|
|
40
|
+
})
|
|
41
|
+
expect(result.current).toBe('resource-project')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('ignores a non-dataset resource (e.g. media library)', () => {
|
|
45
|
+
const {result} = renderHook(() => useResolvedProjectId(), {
|
|
46
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
47
|
+
<ResourceContext.Provider value={{mediaLibraryId: 'ml-id'}}>
|
|
48
|
+
{children}
|
|
49
|
+
</ResourceContext.Provider>
|
|
50
|
+
),
|
|
51
|
+
})
|
|
52
|
+
expect(result.current).toBeUndefined()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns undefined when nothing resolves', () => {
|
|
56
|
+
const {result} = renderHook(() => useResolvedProjectId())
|
|
57
|
+
expect(result.current).toBeUndefined()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {type DocumentResource, isDatasetResource} from '@sanity/sdk'
|
|
2
|
+
import {useContext} from 'react'
|
|
3
|
+
|
|
4
|
+
import {ProjectContext} from '../../context/ProjectContext'
|
|
5
|
+
import {useNormalizedResourceOptions} from './useNormalizedResourceOptions'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the effective `projectId` for project-scoped hooks (`useProject`,
|
|
9
|
+
* `useDatasets`, `useUsers`).
|
|
10
|
+
*
|
|
11
|
+
* Precedence:
|
|
12
|
+
* 1. an explicit `projectId` on the options
|
|
13
|
+
* 2. the ambient project scope (`ProjectContext`, e.g. a dataset-less
|
|
14
|
+
* `<ResourceProvider projectId="…">`)
|
|
15
|
+
* 3. the resolved resource's projectId (`ResourceProvider`/`SDKProvider`)
|
|
16
|
+
*
|
|
17
|
+
* Returns `undefined` when none apply, letting callers fall back to core's
|
|
18
|
+
* `instance.config.projectId`.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export function useResolvedProjectId(options?: {
|
|
23
|
+
projectId?: string
|
|
24
|
+
dataset?: string
|
|
25
|
+
resource?: DocumentResource
|
|
26
|
+
resourceName?: string
|
|
27
|
+
}): string | undefined {
|
|
28
|
+
const {resource} = useNormalizedResourceOptions(options ?? {})
|
|
29
|
+
const contextProjectId = useContext(ProjectContext)
|
|
30
|
+
return (
|
|
31
|
+
options?.projectId ??
|
|
32
|
+
contextProjectId ??
|
|
33
|
+
(resource && isDatasetResource(resource) ? resource.projectId : undefined)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {trackHookMounted} from '@sanity/sdk/_internal'
|
|
3
|
+
import {useRef} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tracks the first usage of a named hook per SDK session.
|
|
9
|
+
* If the telemetry manager hasn't initialized yet, the hook
|
|
10
|
+
* name is buffered and flushed when it becomes available.
|
|
11
|
+
*
|
|
12
|
+
* Uses a ref to ensure the tracking call only happens once per
|
|
13
|
+
* component mount, avoiding repeated WeakMap lookups on re-renders.
|
|
14
|
+
*
|
|
15
|
+
* Call at the top of any public hook whose adoption we want to measure.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function useTrackHookUsage(hookName: string): void {
|
|
20
|
+
const instance = useSanityInstance()
|
|
21
|
+
const tracked = useRef<true | null>(null)
|
|
22
|
+
if (tracked.current === null) {
|
|
23
|
+
tracked.current = true
|
|
24
|
+
trackHookMounted(instance, hookName)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Non-hook variant for tracking hook usage when an instance is already
|
|
30
|
+
* available (avoids an extra `useSanityInstance` call in hooks that
|
|
31
|
+
* already have the instance).
|
|
32
|
+
*
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export function trackHookUsage(instance: SanityInstance, hookName: string): void {
|
|
36
|
+
trackHookMounted(instance, hookName)
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {type Installation, type InstallationActiveConfig} from '@sanity/sdk'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {type FetcherHookResult} from '../helpers/createFetcherHook'
|
|
5
|
+
import {useInstallation} from './useInstallation'
|
|
6
|
+
|
|
7
|
+
test('useInstallation — no include: the base installation', () => {
|
|
8
|
+
const result = useInstallation('inst_1')
|
|
9
|
+
expectTypeOf(result).toEqualTypeOf<FetcherHookResult<Installation<never>>>()
|
|
10
|
+
expectTypeOf<
|
|
11
|
+
Extract<keyof typeof result.data, 'activeConfig' | 'access' | 'interfaces'>
|
|
12
|
+
>().toEqualTypeOf<never>()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('useInstallation — activeConfig include adds the config field', () => {
|
|
16
|
+
const result = useInstallation('inst_1', {include: ['activeConfig']})
|
|
17
|
+
expectTypeOf(result.data).toEqualTypeOf<Installation<'activeConfig'>>()
|
|
18
|
+
expectTypeOf(result.data.activeConfig).toEqualTypeOf<InstallationActiveConfig | null>()
|
|
19
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {type Installation, installation, type InstallationInclude} from '@sanity/sdk'
|
|
2
|
+
|
|
3
|
+
import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a single installation by id.
|
|
7
|
+
*
|
|
8
|
+
* The hook suspends until the first fetch succeeds, so `data` is always present.
|
|
9
|
+
* The `include` tokens you pass shape `data`: each requested token adds its
|
|
10
|
+
* field, and omitted ones are absent from the type.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
* @param installationId - The installation id.
|
|
14
|
+
* @param options - Optional `include` list to expand related resources.
|
|
15
|
+
* @returns The result envelope `{data, isFetching, error, refetch}`.
|
|
16
|
+
*/
|
|
17
|
+
export const useInstallation = createFetcherHook(installation) as <
|
|
18
|
+
Include extends InstallationInclude = never,
|
|
19
|
+
>(
|
|
20
|
+
installationId: string,
|
|
21
|
+
options?: {include?: Include[]},
|
|
22
|
+
) => FetcherHookResult<Installation<Include>>
|