@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2
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 +2488 -2019
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4435 -1529
- package/dist/index.js.map +1 -1
- package/package.json +55 -59
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +30 -18
- 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 +93 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +7 -7
- package/src/components/auth/AuthBoundary.tsx +29 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +104 -61
- 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/ComlinkTokenRefresh.tsx +3 -5
- 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 +93 -90
- package/src/context/ResourceProvider.tsx +62 -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 +2 -2
- package/src/hooks/agent/agentActions.ts +52 -72
- package/src/hooks/agent/useAgentResourceContext.ts +1 -1
- 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/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -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/comlink/useFrameConnection.test.tsx +3 -2
- package/src/hooks/comlink/useFrameConnection.ts +1 -1
- package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
- package/src/hooks/comlink/useWindowConnection.ts +2 -3
- package/src/hooks/comments/useCommentActions.test.tsx +150 -0
- package/src/hooks/comments/useCommentActions.ts +109 -0
- package/src/hooks/comments/useCommentList.ts +79 -0
- package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
- package/src/hooks/comments/useCommentThreads.ts +73 -0
- package/src/hooks/comments/useComments.test.tsx +242 -0
- package/src/hooks/comments/useComments.ts +59 -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 +18 -1
- package/src/hooks/context/useSanityInstance.ts +6 -6
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +11 -17
- 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 +2 -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 +31 -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 +226 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
- 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 +35 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +22 -20
- package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
- package/src/hooks/presence/usePresenceForDocument.ts +104 -0
- package/src/hooks/presence/useReportPresence.test.tsx +202 -0
- package/src/hooks/presence/useReportPresence.ts +170 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +12 -14
- 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 +17 -32
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +94 -17
- 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 +4 -8
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +38 -5
- 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
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import {type DocumentResource, type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {act, render, screen} from '@testing-library/react'
|
|
3
|
-
import {StrictMode, use,
|
|
3
|
+
import {StrictMode, use, useEffect} from 'react'
|
|
4
4
|
import {describe, expect, it, vi} from 'vitest'
|
|
5
5
|
|
|
6
6
|
import {ResourceContext} from './DefaultResourceContext'
|
|
7
|
-
import {
|
|
7
|
+
import {ProjectContext} from './ProjectContext'
|
|
8
8
|
import {ResourceProvider} from './ResourceProvider'
|
|
9
9
|
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
10
10
|
|
|
11
|
-
const testConfig = {
|
|
12
|
-
|
|
11
|
+
const testConfig: SanityConfig = {
|
|
12
|
+
projectId: 'test-project',
|
|
13
|
+
dataset: 'test-dataset',
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function promiseWithResolvers<T = void>(): {
|
|
@@ -58,121 +59,69 @@ describe('ResourceProvider', () => {
|
|
|
58
59
|
consoleSpy.mockRestore()
|
|
59
60
|
})
|
|
60
61
|
|
|
61
|
-
it('
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
useEffect(() => resolve(instance), [instance])
|
|
67
|
-
return null
|
|
62
|
+
it('renders nothing during loading when fallback is null', async () => {
|
|
63
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
64
|
+
const {promise, resolve} = promiseWithResolvers()
|
|
65
|
+
function SuspendingChild(): React.ReactNode {
|
|
66
|
+
throw promise
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
render(
|
|
69
|
+
const {container} = render(
|
|
71
70
|
<ResourceProvider {...testConfig} fallback={null}>
|
|
72
|
-
<
|
|
71
|
+
<SuspendingChild />
|
|
73
72
|
</ResourceProvider>,
|
|
74
73
|
)
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
expect(container).toBeEmptyDOMElement()
|
|
76
|
+
act(() => {
|
|
77
|
+
resolve()
|
|
79
78
|
})
|
|
79
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
80
|
+
consoleSpy.mockRestore()
|
|
80
81
|
})
|
|
81
82
|
|
|
82
|
-
it('
|
|
83
|
-
const
|
|
84
|
-
resource: DocumentResource | undefined
|
|
85
|
-
perspective: unknown
|
|
86
|
-
}>()
|
|
87
|
-
|
|
88
|
-
const CaptureContexts = () => {
|
|
89
|
-
const resource = useContext(ResourceContext)
|
|
90
|
-
const perspective = useContext(PerspectiveContext)
|
|
91
|
-
useEffect(() => captured.resolve({resource, perspective}), [resource, perspective])
|
|
92
|
-
return null
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
render(
|
|
96
|
-
<ResourceProvider
|
|
97
|
-
resource={{projectId: 'abc', dataset: 'prod'}}
|
|
98
|
-
perspective="drafts"
|
|
99
|
-
fallback={null}
|
|
100
|
-
>
|
|
101
|
-
<CaptureContexts />
|
|
102
|
-
</ResourceProvider>,
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
const result = await captured.promise
|
|
106
|
-
expect(result.resource).toEqual({projectId: 'abc', dataset: 'prod'})
|
|
107
|
-
expect(result.perspective).toBe('drafts')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('nested provider overrides resource and perspective via context', async () => {
|
|
111
|
-
const captured = promiseWithResolvers<{
|
|
112
|
-
instance: SanityInstance | null
|
|
113
|
-
resource: DocumentResource | undefined
|
|
114
|
-
perspective: unknown
|
|
115
|
-
}>()
|
|
83
|
+
it('creates root instance when no parent context exists', async () => {
|
|
84
|
+
const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
|
|
116
85
|
|
|
117
|
-
const
|
|
86
|
+
const CaptureInstance = () => {
|
|
118
87
|
const instance = use(SanityInstanceContext)
|
|
119
|
-
|
|
120
|
-
const perspective = useContext(PerspectiveContext)
|
|
121
|
-
useEffect(
|
|
122
|
-
() => captured.resolve({instance, resource, perspective}),
|
|
123
|
-
[instance, resource, perspective],
|
|
124
|
-
)
|
|
88
|
+
useEffect(() => resolve(instance), [instance])
|
|
125
89
|
return null
|
|
126
90
|
}
|
|
127
91
|
|
|
128
92
|
render(
|
|
129
|
-
<ResourceProvider
|
|
130
|
-
<
|
|
131
|
-
resource={{projectId: 'child-proj', dataset: 'child-ds'}}
|
|
132
|
-
perspective="drafts"
|
|
133
|
-
fallback={null}
|
|
134
|
-
>
|
|
135
|
-
<CaptureAll />
|
|
136
|
-
</ResourceProvider>
|
|
93
|
+
<ResourceProvider {...testConfig} fallback={null}>
|
|
94
|
+
<CaptureInstance />
|
|
137
95
|
</ResourceProvider>,
|
|
138
96
|
)
|
|
139
97
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
expect(result.resource).toEqual({projectId: 'child-proj', dataset: 'child-ds'})
|
|
145
|
-
expect(result.perspective).toBe('drafts')
|
|
98
|
+
await expect(promise).resolves.toMatchObject({
|
|
99
|
+
config: testConfig,
|
|
100
|
+
isDisposed: expect.any(Function),
|
|
101
|
+
})
|
|
146
102
|
})
|
|
147
103
|
|
|
148
|
-
it('
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
perspective: unknown
|
|
152
|
-
}>()
|
|
104
|
+
it('reuses instance when parent context exists', async () => {
|
|
105
|
+
const parentConfig: SanityConfig = {...testConfig, dataset: 'parent-dataset'}
|
|
106
|
+
const child = promiseWithResolvers<SanityInstance | null>()
|
|
153
107
|
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
useEffect(() => captured.resolve({resource, perspective}), [resource, perspective])
|
|
108
|
+
const CaptureInstance = () => {
|
|
109
|
+
const childInstance = use(SanityInstanceContext)
|
|
110
|
+
useEffect(() => child.resolve(childInstance), [childInstance])
|
|
158
111
|
return null
|
|
159
112
|
}
|
|
160
113
|
|
|
161
114
|
render(
|
|
162
|
-
<ResourceProvider
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
fallback={null}
|
|
166
|
-
>
|
|
167
|
-
<ResourceProvider fallback={null}>
|
|
168
|
-
<CaptureContexts />
|
|
115
|
+
<ResourceProvider {...parentConfig} fallback={null}>
|
|
116
|
+
<ResourceProvider {...testConfig} fallback={null}>
|
|
117
|
+
<CaptureInstance />
|
|
169
118
|
</ResourceProvider>
|
|
170
119
|
</ResourceProvider>,
|
|
171
120
|
)
|
|
172
121
|
|
|
173
|
-
const
|
|
174
|
-
expect(
|
|
175
|
-
expect(
|
|
122
|
+
const childInstance = await child.promise
|
|
123
|
+
expect(childInstance?.config).toEqual(parentConfig)
|
|
124
|
+
expect(childInstance?.isDisposed()).toBe(false)
|
|
176
125
|
})
|
|
177
126
|
|
|
178
127
|
it('disposes instance when unmounted', async () => {
|
|
@@ -238,4 +187,58 @@ describe('ResourceProvider', () => {
|
|
|
238
187
|
await new Promise((r) => setTimeout(r, 0))
|
|
239
188
|
consoleSpy.mockRestore()
|
|
240
189
|
})
|
|
190
|
+
|
|
191
|
+
it('inherits the missing half of a partial nested config from the parent resource', async () => {
|
|
192
|
+
const {promise, resolve} = promiseWithResolvers<DocumentResource | undefined>()
|
|
193
|
+
const CaptureResource = () => {
|
|
194
|
+
const resource = use(ResourceContext)
|
|
195
|
+
useEffect(() => resolve(resource), [resource])
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
render(
|
|
200
|
+
<ResourceProvider projectId="parent-project" dataset="parent-dataset" fallback={null}>
|
|
201
|
+
{/* Partial config: only `dataset`. It should inherit `projectId` from the
|
|
202
|
+
parent resource and resolve to a complete DatasetResource rather than
|
|
203
|
+
being dropped in favor of the parent's dataset. */}
|
|
204
|
+
<ResourceProvider dataset="child-dataset" fallback={null}>
|
|
205
|
+
<CaptureResource />
|
|
206
|
+
</ResourceProvider>
|
|
207
|
+
</ResourceProvider>,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
await expect(promise).resolves.toEqual({
|
|
211
|
+
projectId: 'parent-project',
|
|
212
|
+
dataset: 'child-dataset',
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('does not merge a nested bare projectId with the enveloping dataset', async () => {
|
|
217
|
+
const captured = promiseWithResolvers<{
|
|
218
|
+
resource: DocumentResource | undefined
|
|
219
|
+
projectId: string | undefined
|
|
220
|
+
}>()
|
|
221
|
+
const Capture = () => {
|
|
222
|
+
const resource = use(ResourceContext)
|
|
223
|
+
const projectId = use(ProjectContext)
|
|
224
|
+
useEffect(() => captured.resolve({resource, projectId}), [resource, projectId])
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
render(
|
|
229
|
+
<ResourceProvider projectId="parent-project" dataset="parent-dataset" fallback={null}>
|
|
230
|
+
{/* Bare `projectId`: switches project scope. It must surface as the
|
|
231
|
+
project but must NOT adopt the parent's dataset — that dataset belongs
|
|
232
|
+
to a different project. */}
|
|
233
|
+
<ResourceProvider projectId="child-project" fallback={null}>
|
|
234
|
+
<Capture />
|
|
235
|
+
</ResourceProvider>
|
|
236
|
+
</ResourceProvider>,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
await expect(captured.promise).resolves.toEqual({
|
|
240
|
+
resource: undefined,
|
|
241
|
+
projectId: 'child-project',
|
|
242
|
+
})
|
|
243
|
+
})
|
|
241
244
|
})
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createSanityInstance,
|
|
3
|
+
type DatasetResource,
|
|
3
4
|
type DocumentResource,
|
|
4
|
-
|
|
5
|
+
isDatasetResource,
|
|
5
6
|
type SanityConfig,
|
|
6
7
|
type SanityInstance,
|
|
7
8
|
} from '@sanity/sdk'
|
|
8
|
-
import {
|
|
9
|
+
import {initTelemetry} from '@sanity/sdk/_internal'
|
|
10
|
+
import {useContext, useEffect, useMemo, useRef, useState} from 'react'
|
|
9
11
|
|
|
10
12
|
import {ResourceContext} from './DefaultResourceContext'
|
|
11
13
|
import {PerspectiveContext} from './PerspectiveContext'
|
|
14
|
+
import {ProjectContext} from './ProjectContext'
|
|
12
15
|
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
16
|
+
import {SanityInstanceProvider} from './SanityInstanceProvider'
|
|
13
17
|
|
|
14
18
|
const DEFAULT_FALLBACK = (
|
|
15
19
|
<>
|
|
@@ -18,15 +22,10 @@ const DEFAULT_FALLBACK = (
|
|
|
18
22
|
)
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
|
-
* Props for the ResourceProvider component
|
|
22
|
-
*
|
|
23
|
-
* Extends `SanityConfig` (minus `defaultResource`) so new config fields are
|
|
24
|
-
* automatically forwarded. The `resource` prop replaces `defaultResource`
|
|
25
|
-
* with a name that better describes its role at the React layer.
|
|
26
|
-
*
|
|
25
|
+
* Props for the ResourceProvider component
|
|
27
26
|
* @internal
|
|
28
27
|
*/
|
|
29
|
-
export interface ResourceProviderProps extends
|
|
28
|
+
export interface ResourceProviderProps extends SanityConfig {
|
|
30
29
|
/**
|
|
31
30
|
* The document resource (project/dataset, media library, or canvas)
|
|
32
31
|
* for this subtree. Hooks that don't specify an explicit resource will
|
|
@@ -46,14 +45,7 @@ export interface ResourceProviderProps extends Omit<SanityConfig, 'defaultResour
|
|
|
46
45
|
*
|
|
47
46
|
* @internal
|
|
48
47
|
*
|
|
49
|
-
* @
|
|
50
|
-
* - **Root usage** (no parent instance): creates a `SanityInstance` with the
|
|
51
|
-
* given config and provides it via `SanityInstanceContext`.
|
|
52
|
-
* - **Nested usage** (inside an existing provider): sets
|
|
53
|
-
* `ResourceContext` and `PerspectiveContext` so hooks in the subtree
|
|
54
|
-
* resolve the correct resource/perspective without creating a new instance.
|
|
55
|
-
*
|
|
56
|
-
* @example Root provider
|
|
48
|
+
* @example
|
|
57
49
|
* ```tsx
|
|
58
50
|
* <ResourceProvider
|
|
59
51
|
* resource={{ projectId: 'your-project-id', dataset: 'production' }}
|
|
@@ -62,57 +54,54 @@ export interface ResourceProviderProps extends Omit<SanityConfig, 'defaultResour
|
|
|
62
54
|
* <YourApp />
|
|
63
55
|
* </ResourceProvider>
|
|
64
56
|
* ```
|
|
65
|
-
*
|
|
66
|
-
* @example Nested override
|
|
67
|
-
* ```tsx
|
|
68
|
-
* <ResourceProvider
|
|
69
|
-
* resource={{ projectId: 'other-project', dataset: 'staging' }}
|
|
70
|
-
* fallback={<LoadingSpinner />}
|
|
71
|
-
* >
|
|
72
|
-
* <SubSection />
|
|
73
|
-
* </ResourceProvider>
|
|
74
|
-
* ```
|
|
75
57
|
*/
|
|
76
58
|
export function ResourceProvider({
|
|
77
59
|
children,
|
|
78
60
|
fallback,
|
|
79
61
|
resource,
|
|
80
|
-
...
|
|
62
|
+
...config
|
|
81
63
|
}: ResourceProviderProps): React.ReactNode {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
[perspective, auth, studio],
|
|
87
|
-
)
|
|
64
|
+
const parentPerspective = useContext(PerspectiveContext)
|
|
65
|
+
const parentResource = useContext(ResourceContext)
|
|
66
|
+
const parentInstance = useContext(SanityInstanceContext)
|
|
67
|
+
const parentProjectId = useContext(ProjectContext)
|
|
88
68
|
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<NestedResourceProvider resource={resource} perspective={perspective} fallback={fallback}>
|
|
92
|
-
{children}
|
|
93
|
-
</NestedResourceProvider>
|
|
94
|
-
)
|
|
95
|
-
}
|
|
69
|
+
const {projectId, dataset, perspective} = config
|
|
96
70
|
|
|
97
|
-
|
|
98
|
-
<RootResourceProvider config={config} resource={resource} fallback={fallback}>
|
|
99
|
-
{children}
|
|
100
|
-
</RootResourceProvider>
|
|
101
|
-
)
|
|
102
|
-
}
|
|
71
|
+
const [instance] = useState<SanityInstance>(() => parentInstance ?? createSanityInstance(config))
|
|
103
72
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
73
|
+
const configResource: DatasetResource | undefined = useMemo(() => {
|
|
74
|
+
// Historically, we allowed asymmetric merging: If you provided JUST a dataset, we'd merge it with the parent projectId.
|
|
75
|
+
//
|
|
76
|
+
// This backwards-compatible merging should be removed in the next major version.
|
|
77
|
+
if (projectId && dataset) return {projectId, dataset}
|
|
78
|
+
if (dataset && parentProjectId) return {projectId: parentProjectId, dataset}
|
|
79
|
+
return undefined
|
|
80
|
+
}, [projectId, dataset, parentProjectId])
|
|
81
|
+
|
|
82
|
+
const effectiveResource = useMemo(() => {
|
|
83
|
+
if (resource) return resource
|
|
84
|
+
if (configResource) return configResource
|
|
85
|
+
// A projectId with no dataset historically created its own scope, so no resource is needed.
|
|
86
|
+
if (projectId) return undefined
|
|
87
|
+
return parentResource
|
|
88
|
+
}, [resource, configResource, projectId, parentResource])
|
|
89
|
+
|
|
90
|
+
// Historically, ResourceProviders allowed a bare `projectId` (no dataset, no resource to complete it) to be provided.
|
|
91
|
+
// This keeps that behavior intact for backwards compatibility, even though we prefer resources everywhere.
|
|
92
|
+
//
|
|
93
|
+
// This should be removed in the next major version.
|
|
94
|
+
const effectiveProjectId = useMemo(() => {
|
|
95
|
+
if (effectiveResource && isDatasetResource(effectiveResource)) {
|
|
96
|
+
return effectiveResource.projectId
|
|
97
|
+
}
|
|
98
|
+
return projectId ?? parentProjectId
|
|
99
|
+
}, [effectiveResource, projectId, parentProjectId])
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (effectiveResource && isDatasetResource(effectiveResource))
|
|
103
|
+
initTelemetry(instance, effectiveResource.projectId)
|
|
104
|
+
}, [instance, effectiveResource])
|
|
116
105
|
|
|
117
106
|
// Ref to hold the scheduled disposal timer.
|
|
118
107
|
const disposal = useRef<{
|
|
@@ -131,47 +120,27 @@ function RootResourceProvider({
|
|
|
131
120
|
disposal.current = {
|
|
132
121
|
instance,
|
|
133
122
|
timeoutId: setTimeout(() => {
|
|
134
|
-
|
|
123
|
+
// don't dispose the parent instance when this unmounts
|
|
124
|
+
if (!instance.isDisposed() && instance !== parentInstance) {
|
|
135
125
|
instance.dispose()
|
|
136
126
|
}
|
|
137
127
|
}, 0),
|
|
138
128
|
}
|
|
139
129
|
}
|
|
140
|
-
}, [instance])
|
|
130
|
+
}, [instance, parentInstance])
|
|
141
131
|
|
|
142
132
|
return (
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
133
|
+
<SanityInstanceProvider
|
|
134
|
+
instance={instance}
|
|
135
|
+
fallback={fallback === undefined ? DEFAULT_FALLBACK : fallback}
|
|
136
|
+
>
|
|
137
|
+
<ResourceContext.Provider value={effectiveResource}>
|
|
138
|
+
<ProjectContext.Provider value={effectiveProjectId}>
|
|
139
|
+
<PerspectiveContext.Provider value={perspective ?? parentPerspective}>
|
|
140
|
+
{children}
|
|
141
|
+
</PerspectiveContext.Provider>
|
|
142
|
+
</ProjectContext.Provider>
|
|
148
143
|
</ResourceContext.Provider>
|
|
149
|
-
</
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function NestedResourceProvider({
|
|
154
|
-
children,
|
|
155
|
-
fallback,
|
|
156
|
-
resource,
|
|
157
|
-
perspective,
|
|
158
|
-
}: {
|
|
159
|
-
children: React.ReactNode
|
|
160
|
-
fallback: React.ReactNode
|
|
161
|
-
resource?: DocumentResource
|
|
162
|
-
perspective?: PerspectiveHandle['perspective']
|
|
163
|
-
}): React.ReactNode {
|
|
164
|
-
const parentResource = useContext(ResourceContext)
|
|
165
|
-
const parentPerspective = useContext(PerspectiveContext)
|
|
166
|
-
|
|
167
|
-
const resolvedResource = resource ?? parentResource
|
|
168
|
-
const resolvedPerspective = perspective ?? parentPerspective
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<ResourceContext.Provider value={resolvedResource}>
|
|
172
|
-
<PerspectiveContext.Provider value={resolvedPerspective}>
|
|
173
|
-
<Suspense fallback={fallback ?? DEFAULT_FALLBACK}>{children}</Suspense>
|
|
174
|
-
</PerspectiveContext.Provider>
|
|
175
|
-
</ResourceContext.Provider>
|
|
144
|
+
</SanityInstanceProvider>
|
|
176
145
|
)
|
|
177
146
|
}
|
|
@@ -5,7 +5,7 @@ import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
|
5
5
|
import {SanityApp} from '../components/SanityApp'
|
|
6
6
|
import {SDKStudioContext, type StudioWorkspaceHandle} from './SDKStudioContext'
|
|
7
7
|
|
|
8
|
-
// Mock SDKProvider to capture the config
|
|
8
|
+
// Mock SDKProvider to capture the config it receives
|
|
9
9
|
const mockSDKProvider = vi.hoisted(() => vi.fn())
|
|
10
10
|
vi.mock('../components/SDKProvider', () => ({
|
|
11
11
|
SDKProvider: mockSDKProvider.mockImplementation(({children}) => (
|
|
@@ -45,31 +45,24 @@ describe('SDKStudioContext', () => {
|
|
|
45
45
|
|
|
46
46
|
expect(mockSDKProvider).toHaveBeenCalled()
|
|
47
47
|
const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
|
|
48
|
-
// config carries studio auth only; no resource fields
|
|
49
48
|
expect(receivedConfig).toMatchObject({
|
|
49
|
+
projectId: 'studio-project-id',
|
|
50
|
+
dataset: 'production',
|
|
50
51
|
studio: {
|
|
51
52
|
auth: {token: mockWorkspace.auth.token},
|
|
52
53
|
},
|
|
53
54
|
})
|
|
54
|
-
expect(receivedConfig).not.toHaveProperty('defaultResource')
|
|
55
|
-
// resource goes into the resources map, not config
|
|
56
|
-
const receivedResources = mockSDKProvider.mock.calls[0][0].resources
|
|
57
|
-
expect(receivedResources).toEqual({
|
|
58
|
-
default: {projectId: 'studio-project-id', dataset: 'production'},
|
|
59
|
-
})
|
|
60
55
|
})
|
|
61
56
|
|
|
62
57
|
it('explicit config takes precedence over SDKStudioContext', () => {
|
|
63
|
-
const explicitConfig: SanityConfig = {
|
|
64
|
-
|
|
58
|
+
const explicitConfig: SanityConfig = {
|
|
59
|
+
projectId: 'explicit-project',
|
|
60
|
+
dataset: 'staging',
|
|
61
|
+
}
|
|
65
62
|
|
|
66
63
|
render(
|
|
67
64
|
<SDKStudioContext.Provider value={mockWorkspace}>
|
|
68
|
-
<SanityApp
|
|
69
|
-
config={explicitConfig}
|
|
70
|
-
resources={explicitResources}
|
|
71
|
-
fallback={<div>Loading</div>}
|
|
72
|
-
>
|
|
65
|
+
<SanityApp config={explicitConfig} fallback={<div>Loading</div>}>
|
|
73
66
|
<div>Child</div>
|
|
74
67
|
</SanityApp>
|
|
75
68
|
</SDKStudioContext.Provider>,
|
|
@@ -77,31 +70,32 @@ describe('SDKStudioContext', () => {
|
|
|
77
70
|
|
|
78
71
|
expect(mockSDKProvider).toHaveBeenCalled()
|
|
79
72
|
const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
|
|
80
|
-
expect(receivedConfig).
|
|
73
|
+
expect(receivedConfig).toMatchObject({
|
|
74
|
+
projectId: 'explicit-project',
|
|
75
|
+
dataset: 'staging',
|
|
76
|
+
})
|
|
77
|
+
// Should NOT have studio config from the context
|
|
81
78
|
expect(receivedConfig.studio).toBeUndefined()
|
|
82
|
-
const receivedResources = mockSDKProvider.mock.calls[0][0].resources
|
|
83
|
-
expect(receivedResources).toEqual(explicitResources)
|
|
84
79
|
})
|
|
85
80
|
|
|
86
81
|
it('SanityApp works without SDKStudioContext (standalone mode)', () => {
|
|
87
|
-
const standaloneConfig: SanityConfig = {
|
|
88
|
-
|
|
82
|
+
const standaloneConfig: SanityConfig = {
|
|
83
|
+
projectId: 'standalone-project',
|
|
84
|
+
dataset: 'production',
|
|
85
|
+
}
|
|
89
86
|
|
|
90
87
|
render(
|
|
91
|
-
<SanityApp
|
|
92
|
-
config={standaloneConfig}
|
|
93
|
-
resources={standaloneResources}
|
|
94
|
-
fallback={<div>Loading</div>}
|
|
95
|
-
>
|
|
88
|
+
<SanityApp config={standaloneConfig} fallback={<div>Loading</div>}>
|
|
96
89
|
<div>Child</div>
|
|
97
90
|
</SanityApp>,
|
|
98
91
|
)
|
|
99
92
|
|
|
100
93
|
expect(mockSDKProvider).toHaveBeenCalled()
|
|
101
94
|
const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
|
|
102
|
-
expect(receivedConfig).
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
expect(receivedConfig).toMatchObject({
|
|
96
|
+
projectId: 'standalone-project',
|
|
97
|
+
dataset: 'production',
|
|
98
|
+
})
|
|
105
99
|
})
|
|
106
100
|
|
|
107
101
|
it('handles workspace without auth.token (older Studio)', () => {
|
|
@@ -121,11 +115,12 @@ describe('SDKStudioContext', () => {
|
|
|
121
115
|
|
|
122
116
|
expect(mockSDKProvider).toHaveBeenCalled()
|
|
123
117
|
const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
|
|
124
|
-
expect(receivedConfig).toMatchObject({
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
expect(receivedResources).toEqual({
|
|
128
|
-
default: {projectId: 'older-studio', dataset: 'production'},
|
|
118
|
+
expect(receivedConfig).toMatchObject({
|
|
119
|
+
projectId: 'older-studio',
|
|
120
|
+
dataset: 'production',
|
|
129
121
|
})
|
|
122
|
+
// studio config should be present but auth.token should be undefined
|
|
123
|
+
expect(receivedConfig.studio).toBeDefined()
|
|
124
|
+
expect(receivedConfig.studio?.auth).toBeUndefined()
|
|
130
125
|
})
|
|
131
126
|
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {act, render, screen} from '@testing-library/react'
|
|
3
|
+
import {use, useEffect} from 'react'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
7
|
+
import {SanityInstanceProvider} from './SanityInstanceProvider'
|
|
8
|
+
|
|
9
|
+
function promiseWithResolvers<T = void>(): {
|
|
10
|
+
promise: Promise<T>
|
|
11
|
+
resolve: (t: T) => void
|
|
12
|
+
reject: (error: unknown) => void
|
|
13
|
+
} {
|
|
14
|
+
let resolve!: (t: T) => void
|
|
15
|
+
let reject!: (error: unknown) => void
|
|
16
|
+
const promise = new Promise<T>((res, rej) => {
|
|
17
|
+
resolve = res
|
|
18
|
+
reject = rej
|
|
19
|
+
})
|
|
20
|
+
return {resolve, reject, promise}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('SanityInstanceProvider', () => {
|
|
24
|
+
it('renders children', () => {
|
|
25
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
26
|
+
|
|
27
|
+
render(
|
|
28
|
+
<SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
|
|
29
|
+
<div data-testid="test-child">Child Component</div>
|
|
30
|
+
</SanityInstanceProvider>,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
expect(screen.getByTestId('test-child')).toBeInTheDocument()
|
|
34
|
+
instance.dispose()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('provides the given instance via context', async () => {
|
|
38
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
39
|
+
const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
|
|
40
|
+
|
|
41
|
+
const CaptureInstance = () => {
|
|
42
|
+
const ctx = use(SanityInstanceContext)
|
|
43
|
+
useEffect(() => resolve(ctx), [ctx])
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<SanityInstanceProvider instance={instance} fallback={null}>
|
|
49
|
+
<CaptureInstance />
|
|
50
|
+
</SanityInstanceProvider>,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const provided = await promise
|
|
54
|
+
expect(provided).toBe(instance)
|
|
55
|
+
instance.dispose()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('shows fallback during suspense', async () => {
|
|
59
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
60
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
61
|
+
const {promise, resolve} = promiseWithResolvers()
|
|
62
|
+
|
|
63
|
+
function SuspendingChild(): React.ReactNode {
|
|
64
|
+
throw promise
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<SanityInstanceProvider
|
|
69
|
+
instance={instance}
|
|
70
|
+
fallback={<div data-testid="fallback">Loading...</div>}
|
|
71
|
+
>
|
|
72
|
+
<SuspendingChild />
|
|
73
|
+
</SanityInstanceProvider>,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(screen.getByTestId('fallback')).toBeInTheDocument()
|
|
77
|
+
act(() => {
|
|
78
|
+
resolve()
|
|
79
|
+
})
|
|
80
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
81
|
+
instance.dispose()
|
|
82
|
+
consoleSpy.mockRestore()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('does not dispose the instance on unmount', async () => {
|
|
86
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
87
|
+
|
|
88
|
+
const {unmount} = render(
|
|
89
|
+
<SanityInstanceProvider instance={instance} fallback={null}>
|
|
90
|
+
<div />
|
|
91
|
+
</SanityInstanceProvider>,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
unmount()
|
|
95
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
96
|
+
|
|
97
|
+
expect(instance.isDisposed()).toBe(false)
|
|
98
|
+
instance.dispose()
|
|
99
|
+
})
|
|
100
|
+
})
|