@sanity/sdk-react 2.6.0 → 2.8.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/README.md +68 -0
- package/dist/index.d.ts +544 -20
- package/dist/index.js +118 -72
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/_exports/sdk-react.ts +1 -0
- package/src/components/SanityApp.test.tsx +72 -2
- package/src/components/SanityApp.tsx +52 -10
- package/src/components/auth/AuthBoundary.test.tsx +3 -0
- package/src/components/auth/AuthBoundary.tsx +5 -5
- package/src/components/auth/LoginError.test.tsx +5 -0
- package/src/components/auth/LoginError.tsx +22 -1
- package/src/context/ComlinkTokenRefresh.test.tsx +2 -2
- package/src/context/ComlinkTokenRefresh.tsx +3 -2
- package/src/context/SDKStudioContext.test.tsx +126 -0
- package/src/context/SDKStudioContext.ts +65 -0
- package/src/hooks/agent/agentActions.ts +436 -21
- package/src/hooks/dashboard/useDispatchIntent.test.ts +5 -5
- package/src/hooks/dashboard/useDispatchIntent.ts +5 -5
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +2 -3
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +3 -3
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +85 -0
- package/src/hooks/projection/useDocumentProjection.ts +15 -4
- package/src/hooks/query/useQuery.ts +23 -17
- package/src/hooks/context/useSource.tsx +0 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"browserslist": "extends @sanity/browserslist-config",
|
|
44
44
|
"prettier": "@sanity/prettier-config",
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@sanity/client": "^7.
|
|
46
|
+
"@sanity/client": "^7.14.1",
|
|
47
47
|
"@sanity/message-protocol": "^0.18.0",
|
|
48
48
|
"@sanity/types": "^5.2.0",
|
|
49
49
|
"@types/lodash-es": "^4.17.12",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"react-compiler-runtime": "19.1.0-rc.2",
|
|
53
53
|
"react-error-boundary": "^5.0.0",
|
|
54
54
|
"rxjs": "^7.8.2",
|
|
55
|
-
"@sanity/sdk": "2.
|
|
55
|
+
"@sanity/sdk": "2.8.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@sanity/browserslist-config": "^1.0.5",
|
|
@@ -77,9 +77,9 @@
|
|
|
77
77
|
"typescript": "^5.8.3",
|
|
78
78
|
"vite": "^6.3.4",
|
|
79
79
|
"vitest": "^3.2.4",
|
|
80
|
+
"@repo/package.bundle": "3.82.0",
|
|
80
81
|
"@repo/config-eslint": "0.0.0",
|
|
81
82
|
"@repo/config-test": "0.0.1",
|
|
82
|
-
"@repo/package.bundle": "3.82.0",
|
|
83
83
|
"@repo/package.config": "0.0.1",
|
|
84
84
|
"@repo/tsconfig": "0.0.1"
|
|
85
85
|
},
|
|
@@ -7,6 +7,7 @@ export {SDKProvider, type SDKProviderProps} from '../components/SDKProvider'
|
|
|
7
7
|
export {ComlinkTokenRefreshProvider} from '../context/ComlinkTokenRefresh'
|
|
8
8
|
export {renderSanityApp} from '../context/renderSanityApp'
|
|
9
9
|
export {ResourceProvider, type ResourceProviderProps} from '../context/ResourceProvider'
|
|
10
|
+
export {SDKStudioContext, type StudioWorkspaceHandle} from '../context/SDKStudioContext'
|
|
10
11
|
export {
|
|
11
12
|
useAgentGenerate,
|
|
12
13
|
useAgentPatch,
|
|
@@ -207,6 +207,37 @@ describe('SanityApp', () => {
|
|
|
207
207
|
consoleWarnSpy.mockRestore()
|
|
208
208
|
})
|
|
209
209
|
|
|
210
|
+
it('redirects to core if config is omitted and no studio context is available', async () => {
|
|
211
|
+
const originalLocation = window.location
|
|
212
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
213
|
+
|
|
214
|
+
const mockLocation = {
|
|
215
|
+
replace: vi.fn(),
|
|
216
|
+
href: 'http://sanity-test.app',
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Object.defineProperty(window, 'location', {
|
|
220
|
+
value: mockLocation,
|
|
221
|
+
writable: true,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
render(
|
|
225
|
+
<SanityApp fallback={<div>Fallback</div>}>
|
|
226
|
+
<div>Test Child</div>
|
|
227
|
+
</SanityApp>,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 1010))
|
|
231
|
+
|
|
232
|
+
expect(mockLocation.replace).toHaveBeenCalledWith('https://sanity.io/welcome')
|
|
233
|
+
|
|
234
|
+
Object.defineProperty(window, 'location', {
|
|
235
|
+
value: originalLocation,
|
|
236
|
+
writable: true,
|
|
237
|
+
})
|
|
238
|
+
consoleWarnSpy.mockRestore()
|
|
239
|
+
})
|
|
240
|
+
|
|
210
241
|
it('does not redirect to core if not inside iframe and local url', async () => {
|
|
211
242
|
const originalLocation = window.location
|
|
212
243
|
|
|
@@ -244,7 +275,7 @@ describe('SanityApp', () => {
|
|
|
244
275
|
})
|
|
245
276
|
})
|
|
246
277
|
|
|
247
|
-
it('does not redirect to core if
|
|
278
|
+
it('does not redirect to core if studio config is provided', async () => {
|
|
248
279
|
const originalLocation = window.location
|
|
249
280
|
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
250
281
|
|
|
@@ -256,7 +287,7 @@ describe('SanityApp', () => {
|
|
|
256
287
|
const mockSanityConfig: SanityConfig = {
|
|
257
288
|
projectId: 'test-project',
|
|
258
289
|
dataset: 'test-dataset',
|
|
259
|
-
|
|
290
|
+
studio: {},
|
|
260
291
|
}
|
|
261
292
|
|
|
262
293
|
Object.defineProperty(window, 'location', {
|
|
@@ -283,4 +314,43 @@ describe('SanityApp', () => {
|
|
|
283
314
|
})
|
|
284
315
|
consoleWarnSpy.mockRestore()
|
|
285
316
|
})
|
|
317
|
+
|
|
318
|
+
it('does not redirect to core when studio config is provided', async () => {
|
|
319
|
+
const originalLocation = window.location
|
|
320
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
321
|
+
|
|
322
|
+
const mockLocation = {
|
|
323
|
+
replace: vi.fn(),
|
|
324
|
+
href: 'http://sanity-test.app',
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const mockSanityConfig: SanityConfig = {
|
|
328
|
+
projectId: 'test-project',
|
|
329
|
+
dataset: 'test-dataset',
|
|
330
|
+
studio: {},
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
Object.defineProperty(window, 'location', {
|
|
334
|
+
value: mockLocation,
|
|
335
|
+
writable: true,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
render(
|
|
339
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
340
|
+
<div>Test Child</div>
|
|
341
|
+
</SanityApp>,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
// Wait for 1 second
|
|
345
|
+
await new Promise((resolve) => setTimeout(resolve, 1010))
|
|
346
|
+
|
|
347
|
+
expect(mockLocation.replace).not.toHaveBeenCalled()
|
|
348
|
+
|
|
349
|
+
// Clean up the mock
|
|
350
|
+
Object.defineProperty(window, 'location', {
|
|
351
|
+
value: originalLocation,
|
|
352
|
+
writable: true,
|
|
353
|
+
})
|
|
354
|
+
consoleWarnSpy.mockRestore()
|
|
355
|
+
})
|
|
286
356
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {type DocumentSource, type SanityConfig} from '@sanity/sdk'
|
|
2
|
-
import {type ReactElement, useEffect} from 'react'
|
|
1
|
+
import {type DocumentSource, isStudioConfig, type SanityConfig} from '@sanity/sdk'
|
|
2
|
+
import {type ReactElement, useContext, useEffect, useMemo} from 'react'
|
|
3
3
|
|
|
4
|
+
import {SDKStudioContext, type StudioWorkspaceHandle} from '../context/SDKStudioContext'
|
|
4
5
|
import {SDKProvider} from './SDKProvider'
|
|
5
6
|
import {isInIframe, isLocalUrl} from './utils'
|
|
6
7
|
|
|
@@ -9,8 +10,13 @@ import {isInIframe, isLocalUrl} from './utils'
|
|
|
9
10
|
* @category Types
|
|
10
11
|
*/
|
|
11
12
|
export interface SanityAppProps {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* One or more SanityConfig objects providing a project ID and dataset name.
|
|
15
|
+
* Optional when `SanityApp` is rendered inside an `SDKStudioContext` provider
|
|
16
|
+
* (e.g. inside Sanity Studio) — the config is derived from the workspace
|
|
17
|
+
* automatically.
|
|
18
|
+
*/
|
|
19
|
+
config?: SanityConfig | SanityConfig[]
|
|
14
20
|
/** @deprecated use the `config` prop instead. */
|
|
15
21
|
sanityConfigs?: SanityConfig[]
|
|
16
22
|
sources?: Record<string, DocumentSource>
|
|
@@ -21,6 +27,21 @@ export interface SanityAppProps {
|
|
|
21
27
|
|
|
22
28
|
const REDIRECT_URL = 'https://sanity.io/welcome'
|
|
23
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Derive a SanityConfig from a Studio workspace handle.
|
|
32
|
+
* Maps the workspace's projectId, dataset, and reactive auth token into
|
|
33
|
+
* the SDK's config shape.
|
|
34
|
+
*/
|
|
35
|
+
function deriveConfigFromWorkspace(workspace: StudioWorkspaceHandle): SanityConfig {
|
|
36
|
+
return {
|
|
37
|
+
projectId: workspace.projectId,
|
|
38
|
+
dataset: workspace.dataset,
|
|
39
|
+
studio: {
|
|
40
|
+
auth: workspace.auth.token ? {token: workspace.auth.token} : undefined,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
/**
|
|
25
46
|
* @public
|
|
26
47
|
*
|
|
@@ -29,12 +50,18 @@ const REDIRECT_URL = 'https://sanity.io/welcome'
|
|
|
29
50
|
* must be wrapped with the SanityApp component to function properly.
|
|
30
51
|
*
|
|
31
52
|
* The `config` prop on the SanityApp component accepts either a single {@link SanityConfig} object, or an array of them.
|
|
32
|
-
* This allows your app to work with one or more of your organization
|
|
53
|
+
* This allows your app to work with one or more of your organization's datasets.
|
|
54
|
+
*
|
|
55
|
+
* When rendered inside a Sanity Studio that provides `SDKStudioContext`, the `config` prop is
|
|
56
|
+
* optional — `SanityApp` will automatically derive `projectId`, `dataset`, and auth from the
|
|
57
|
+
* Studio workspace.
|
|
33
58
|
*
|
|
34
59
|
* @remarks
|
|
35
60
|
* When passing multiple SanityConfig objects to the `config` prop, the first configuration in the array becomes the default
|
|
36
61
|
* configuration used by the App SDK Hooks.
|
|
37
62
|
*
|
|
63
|
+
* When both `config` and `SDKStudioContext` are available, the explicit `config` takes precedence.
|
|
64
|
+
*
|
|
38
65
|
* @category Components
|
|
39
66
|
* @param props - Your Sanity configuration and the React children to render
|
|
40
67
|
* @returns Your Sanity application, integrated with your Sanity configuration and application context
|
|
@@ -82,14 +109,29 @@ const REDIRECT_URL = 'https://sanity.io/welcome'
|
|
|
82
109
|
export function SanityApp({
|
|
83
110
|
children,
|
|
84
111
|
fallback,
|
|
85
|
-
config
|
|
112
|
+
config: configProp,
|
|
86
113
|
...props
|
|
87
114
|
}: SanityAppProps): ReactElement {
|
|
115
|
+
const studioWorkspace = useContext(SDKStudioContext)
|
|
116
|
+
|
|
117
|
+
// Derive config: explicit config takes precedence, then Studio context
|
|
118
|
+
const resolvedConfig = useMemo(() => {
|
|
119
|
+
if (configProp) return configProp
|
|
120
|
+
if (studioWorkspace) return deriveConfigFromWorkspace(studioWorkspace)
|
|
121
|
+
return []
|
|
122
|
+
}, [configProp, studioWorkspace])
|
|
123
|
+
|
|
88
124
|
useEffect(() => {
|
|
89
125
|
let timeout: NodeJS.Timeout | undefined
|
|
90
|
-
const primaryConfig = Array.isArray(
|
|
126
|
+
const primaryConfig = Array.isArray(resolvedConfig) ? resolvedConfig[0] : resolvedConfig
|
|
127
|
+
const shouldRedirectWithoutConfig =
|
|
128
|
+
configProp === undefined && !studioWorkspace && !primaryConfig
|
|
91
129
|
|
|
92
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
!isInIframe() &&
|
|
132
|
+
!isLocalUrl(window) &&
|
|
133
|
+
(shouldRedirectWithoutConfig || (!!primaryConfig && !isStudioConfig(primaryConfig)))
|
|
134
|
+
) {
|
|
93
135
|
// If the app is not running in an iframe and is not a local url, redirect to core.
|
|
94
136
|
timeout = setTimeout(() => {
|
|
95
137
|
// eslint-disable-next-line no-console
|
|
@@ -98,10 +140,10 @@ export function SanityApp({
|
|
|
98
140
|
}, 1000)
|
|
99
141
|
}
|
|
100
142
|
return () => clearTimeout(timeout)
|
|
101
|
-
}, [
|
|
143
|
+
}, [configProp, resolvedConfig, studioWorkspace])
|
|
102
144
|
|
|
103
145
|
return (
|
|
104
|
-
<SDKProvider {...props} fallback={fallback} config={
|
|
146
|
+
<SDKProvider {...props} fallback={fallback} config={resolvedConfig}>
|
|
105
147
|
{children}
|
|
106
148
|
</SDKProvider>
|
|
107
149
|
)
|
|
@@ -22,6 +22,9 @@ vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
|
22
22
|
vi.mock('../../hooks/auth/useLogOut', () => ({
|
|
23
23
|
useLogOut: vi.fn(() => async () => {}),
|
|
24
24
|
}))
|
|
25
|
+
vi.mock('../../hooks/comlink/useWindowConnection', () => ({
|
|
26
|
+
useWindowConnection: vi.fn(() => ({fetch: vi.fn()})),
|
|
27
|
+
}))
|
|
25
28
|
|
|
26
29
|
// Mock AuthError throwing scenario
|
|
27
30
|
vi.mock('./AuthError', async (importOriginal) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {CorsOriginError} from '@sanity/client'
|
|
2
|
-
import {AuthStateType, getCorsErrorProjectId} from '@sanity/sdk'
|
|
2
|
+
import {AuthStateType, getCorsErrorProjectId, isStudioConfig} from '@sanity/sdk'
|
|
3
3
|
import {useEffect, useMemo} from 'react'
|
|
4
4
|
import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
|
|
5
5
|
|
|
@@ -156,20 +156,20 @@ function AuthSwitch({
|
|
|
156
156
|
}: AuthSwitchProps) {
|
|
157
157
|
const authState = useAuthState()
|
|
158
158
|
const instance = useSanityInstance()
|
|
159
|
-
const
|
|
159
|
+
const isStudio = isStudioConfig(instance.config)
|
|
160
160
|
const disableVerifyOrg =
|
|
161
|
-
!verifyOrganization ||
|
|
161
|
+
!verifyOrganization || isStudio || authState.type !== AuthStateType.LOGGED_IN
|
|
162
162
|
const orgError = useVerifyOrgProjects(disableVerifyOrg, projectIds)
|
|
163
163
|
|
|
164
164
|
const isLoggedOut = authState.type === AuthStateType.LOGGED_OUT && !authState.isDestroyingSession
|
|
165
165
|
const loginUrl = useLoginUrl()
|
|
166
166
|
|
|
167
167
|
useEffect(() => {
|
|
168
|
-
if (isLoggedOut && !isInIframe() && !
|
|
168
|
+
if (isLoggedOut && !isInIframe() && !isStudio) {
|
|
169
169
|
// We don't want to redirect to login if we're in the Dashboard nor in studio mode
|
|
170
170
|
window.location.href = loginUrl
|
|
171
171
|
}
|
|
172
|
-
}, [isLoggedOut, loginUrl,
|
|
172
|
+
}, [isLoggedOut, loginUrl, isStudio])
|
|
173
173
|
|
|
174
174
|
// Only check the error if verification is enabled
|
|
175
175
|
if (verifyOrganization && orgError) {
|
|
@@ -9,6 +9,10 @@ vi.mock('../../hooks/auth/useLogOut', () => ({
|
|
|
9
9
|
useLogOut: vi.fn(() => async () => {}),
|
|
10
10
|
}))
|
|
11
11
|
|
|
12
|
+
vi.mock('../../hooks/comlink/useWindowConnection', () => ({
|
|
13
|
+
useWindowConnection: vi.fn(() => ({fetch: vi.fn()})),
|
|
14
|
+
}))
|
|
15
|
+
|
|
12
16
|
describe('LoginError', () => {
|
|
13
17
|
it('shows authentication error and retry button', async () => {
|
|
14
18
|
const mockReset = vi.fn()
|
|
@@ -21,6 +25,7 @@ describe('LoginError', () => {
|
|
|
21
25
|
)
|
|
22
26
|
|
|
23
27
|
expect(screen.getByText('Authentication Error')).toBeInTheDocument()
|
|
28
|
+
|
|
24
29
|
const retryButton = screen.getByRole('button', {name: 'Retry'})
|
|
25
30
|
fireEvent.click(retryButton)
|
|
26
31
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {ClientError} from '@sanity/client'
|
|
2
|
+
import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
2
3
|
import {
|
|
3
4
|
AuthStateType,
|
|
4
5
|
getClientErrorApiBody,
|
|
@@ -10,6 +11,8 @@ import {type FallbackProps} from 'react-error-boundary'
|
|
|
10
11
|
|
|
11
12
|
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
12
13
|
import {useLogOut} from '../../hooks/auth/useLogOut'
|
|
14
|
+
import {useWindowConnection} from '../../hooks/comlink/useWindowConnection'
|
|
15
|
+
import {useSanityInstance} from '../../hooks/context/useSanityInstance'
|
|
13
16
|
import {Error} from '../errors/Error'
|
|
14
17
|
import {AuthError} from './AuthError'
|
|
15
18
|
import {ConfigurationError} from './ConfigurationError'
|
|
@@ -36,12 +39,23 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
|
|
|
36
39
|
|
|
37
40
|
const logout = useLogOut()
|
|
38
41
|
const authState = useAuthState()
|
|
42
|
+
const {
|
|
43
|
+
config: {projectId},
|
|
44
|
+
} = useSanityInstance()
|
|
39
45
|
|
|
40
46
|
const [authErrorMessage, setAuthErrorMessage] = useState(
|
|
41
47
|
'Please try again or contact support if the problem persists.',
|
|
42
48
|
)
|
|
43
49
|
const [showRetryCta, setShowRetryCta] = useState(true)
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* TODO: before merge update message-protocol package to include the new message type
|
|
53
|
+
*/
|
|
54
|
+
const {fetch} = useWindowConnection({
|
|
55
|
+
name: SDK_NODE_NAME,
|
|
56
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
57
|
+
})
|
|
58
|
+
|
|
45
59
|
const handleRetry = useCallback(async () => {
|
|
46
60
|
await logout()
|
|
47
61
|
resetErrorBoundary()
|
|
@@ -55,6 +69,13 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
|
|
|
55
69
|
const description = getClientErrorApiDescription(error)
|
|
56
70
|
if (description) setAuthErrorMessage(description)
|
|
57
71
|
setShowRetryCta(false)
|
|
72
|
+
/**
|
|
73
|
+
* Handoff to dashboard to enable the request access flow for the project.
|
|
74
|
+
*/
|
|
75
|
+
fetch('dashboard/v1/auth/access/request', {
|
|
76
|
+
resourceType: 'project',
|
|
77
|
+
resourceId: projectId,
|
|
78
|
+
})
|
|
58
79
|
} else {
|
|
59
80
|
setShowRetryCta(true)
|
|
60
81
|
handleRetry()
|
|
@@ -73,7 +94,7 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
|
|
|
73
94
|
setAuthErrorMessage(error.message)
|
|
74
95
|
setShowRetryCta(true)
|
|
75
96
|
}
|
|
76
|
-
}, [authState, handleRetry, error])
|
|
97
|
+
}, [authState, handleRetry, error, fetch, projectId])
|
|
77
98
|
|
|
78
99
|
return (
|
|
79
100
|
<Error
|
|
@@ -334,9 +334,9 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
334
334
|
})
|
|
335
335
|
|
|
336
336
|
describe('when in studio mode', () => {
|
|
337
|
-
it('should not render DashboardTokenRefresh when studio
|
|
337
|
+
it('should not render DashboardTokenRefresh when studio config is provided', () => {
|
|
338
338
|
render(
|
|
339
|
-
<ResourceProvider fallback={null}
|
|
339
|
+
<ResourceProvider fallback={null} studio={{}}>
|
|
340
340
|
<ComlinkTokenRefreshProvider>
|
|
341
341
|
<div>Test</div>
|
|
342
342
|
</ComlinkTokenRefreshProvider>
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
AuthStateType,
|
|
5
5
|
type FrameMessage,
|
|
6
6
|
getIsInDashboardState,
|
|
7
|
+
isStudioConfig,
|
|
7
8
|
type NewTokenResponseMessage,
|
|
8
9
|
type RequestNewTokenMessage,
|
|
9
10
|
setAuthToken,
|
|
@@ -130,9 +131,9 @@ function DashboardTokenRefresh({children}: PropsWithChildren) {
|
|
|
130
131
|
export const ComlinkTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
|
|
131
132
|
const instance = useSanityInstance()
|
|
132
133
|
const isInDashboard = useMemo(() => getIsInDashboardState(instance).getCurrent(), [instance])
|
|
133
|
-
const
|
|
134
|
+
const isStudio = isStudioConfig(instance.config)
|
|
134
135
|
|
|
135
|
-
if (isInDashboard && !
|
|
136
|
+
if (isInDashboard && !isStudio) {
|
|
136
137
|
return <DashboardTokenRefresh>{children}</DashboardTokenRefresh>
|
|
137
138
|
}
|
|
138
139
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {type SanityConfig} from '@sanity/sdk'
|
|
2
|
+
import {render} from '@testing-library/react'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {SanityApp} from '../components/SanityApp'
|
|
6
|
+
import {SDKStudioContext, type StudioWorkspaceHandle} from './SDKStudioContext'
|
|
7
|
+
|
|
8
|
+
// Mock SDKProvider to capture the config it receives
|
|
9
|
+
const mockSDKProvider = vi.hoisted(() => vi.fn())
|
|
10
|
+
vi.mock('../components/SDKProvider', () => ({
|
|
11
|
+
SDKProvider: mockSDKProvider.mockImplementation(({children}) => (
|
|
12
|
+
<div data-testid="sdk-provider">{children}</div>
|
|
13
|
+
)),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
// Mock utils to prevent redirect logic
|
|
17
|
+
vi.mock('../components/utils', () => ({
|
|
18
|
+
isInIframe: () => true,
|
|
19
|
+
isLocalUrl: () => true,
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
describe('SDKStudioContext', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mockSDKProvider.mockClear()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const mockWorkspace: StudioWorkspaceHandle = {
|
|
28
|
+
projectId: 'studio-project-id',
|
|
29
|
+
dataset: 'production',
|
|
30
|
+
auth: {
|
|
31
|
+
token: {
|
|
32
|
+
subscribe: vi.fn(() => ({unsubscribe: vi.fn()})),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it('SanityApp derives config from SDKStudioContext when no config prop is given', () => {
|
|
38
|
+
render(
|
|
39
|
+
<SDKStudioContext.Provider value={mockWorkspace}>
|
|
40
|
+
<SanityApp fallback={<div>Loading</div>}>
|
|
41
|
+
<div>Child</div>
|
|
42
|
+
</SanityApp>
|
|
43
|
+
</SDKStudioContext.Provider>,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
expect(mockSDKProvider).toHaveBeenCalled()
|
|
47
|
+
const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
|
|
48
|
+
expect(receivedConfig).toMatchObject({
|
|
49
|
+
projectId: 'studio-project-id',
|
|
50
|
+
dataset: 'production',
|
|
51
|
+
studio: {
|
|
52
|
+
auth: {token: mockWorkspace.auth.token},
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('explicit config takes precedence over SDKStudioContext', () => {
|
|
58
|
+
const explicitConfig: SanityConfig = {
|
|
59
|
+
projectId: 'explicit-project',
|
|
60
|
+
dataset: 'staging',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<SDKStudioContext.Provider value={mockWorkspace}>
|
|
65
|
+
<SanityApp config={explicitConfig} fallback={<div>Loading</div>}>
|
|
66
|
+
<div>Child</div>
|
|
67
|
+
</SanityApp>
|
|
68
|
+
</SDKStudioContext.Provider>,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(mockSDKProvider).toHaveBeenCalled()
|
|
72
|
+
const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
|
|
73
|
+
expect(receivedConfig).toMatchObject({
|
|
74
|
+
projectId: 'explicit-project',
|
|
75
|
+
dataset: 'staging',
|
|
76
|
+
})
|
|
77
|
+
// Should NOT have studio config from the context
|
|
78
|
+
expect(receivedConfig.studio).toBeUndefined()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('SanityApp works without SDKStudioContext (standalone mode)', () => {
|
|
82
|
+
const standaloneConfig: SanityConfig = {
|
|
83
|
+
projectId: 'standalone-project',
|
|
84
|
+
dataset: 'production',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<SanityApp config={standaloneConfig} fallback={<div>Loading</div>}>
|
|
89
|
+
<div>Child</div>
|
|
90
|
+
</SanityApp>,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
expect(mockSDKProvider).toHaveBeenCalled()
|
|
94
|
+
const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
|
|
95
|
+
expect(receivedConfig).toMatchObject({
|
|
96
|
+
projectId: 'standalone-project',
|
|
97
|
+
dataset: 'production',
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('handles workspace without auth.token (older Studio)', () => {
|
|
102
|
+
const olderWorkspace: StudioWorkspaceHandle = {
|
|
103
|
+
projectId: 'older-studio',
|
|
104
|
+
dataset: 'production',
|
|
105
|
+
auth: {},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
render(
|
|
109
|
+
<SDKStudioContext.Provider value={olderWorkspace}>
|
|
110
|
+
<SanityApp fallback={<div>Loading</div>}>
|
|
111
|
+
<div>Child</div>
|
|
112
|
+
</SanityApp>
|
|
113
|
+
</SDKStudioContext.Provider>,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
expect(mockSDKProvider).toHaveBeenCalled()
|
|
117
|
+
const receivedConfig = mockSDKProvider.mock.calls[0][0].config as SanityConfig
|
|
118
|
+
expect(receivedConfig).toMatchObject({
|
|
119
|
+
projectId: 'older-studio',
|
|
120
|
+
dataset: 'production',
|
|
121
|
+
})
|
|
122
|
+
// studio config should be present but auth.token should be undefined
|
|
123
|
+
expect(receivedConfig.studio).toBeDefined()
|
|
124
|
+
expect(receivedConfig.studio?.auth).toBeUndefined()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {type TokenSource} from '@sanity/sdk'
|
|
2
|
+
import {createContext} from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal duck-typed interface representing a Sanity Studio workspace.
|
|
6
|
+
* The Studio's `Workspace` type satisfies this naturally — no import
|
|
7
|
+
* dependency on the `sanity` package is required.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export interface StudioWorkspaceHandle {
|
|
12
|
+
/** The Sanity project ID for this workspace. */
|
|
13
|
+
projectId: string
|
|
14
|
+
/** The dataset name for this workspace. */
|
|
15
|
+
dataset: string
|
|
16
|
+
/** Authentication state for this workspace. */
|
|
17
|
+
auth: {
|
|
18
|
+
/**
|
|
19
|
+
* Reactive token source from the Studio's auth store.
|
|
20
|
+
* When present, the SDK subscribes for ongoing token sync — the Studio
|
|
21
|
+
* is the single authority for auth and handles token refresh.
|
|
22
|
+
*
|
|
23
|
+
* Optional because Studios before Aug 2022 may not expose it. When
|
|
24
|
+
* absent, the SDK falls back to localStorage/cookie discovery.
|
|
25
|
+
*/
|
|
26
|
+
token?: TokenSource
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* React context that allows the SDK to auto-detect when it is running
|
|
32
|
+
* inside a Sanity Studio. The Studio provides a workspace handle via this
|
|
33
|
+
* context, and `SanityApp` reads it to derive `projectId`, `dataset`, and
|
|
34
|
+
* a reactive auth token — eliminating the need for manual configuration.
|
|
35
|
+
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* This context is defined in `@sanity/sdk-react` and provided by the Studio.
|
|
38
|
+
* The Studio imports it from this package and wraps its component tree:
|
|
39
|
+
*
|
|
40
|
+
* ```tsx
|
|
41
|
+
* import {SDKStudioContext} from '@sanity/sdk-react'
|
|
42
|
+
*
|
|
43
|
+
* function StudioRoot({children}) {
|
|
44
|
+
* const workspace = useWorkspace()
|
|
45
|
+
* return (
|
|
46
|
+
* <SDKStudioContext.Provider value={workspace}>
|
|
47
|
+
* {children}
|
|
48
|
+
* </SDKStudioContext.Provider>
|
|
49
|
+
* )
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* When `SanityApp` is rendered inside this provider, it auto-configures
|
|
54
|
+
* without any `config` prop:
|
|
55
|
+
*
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <SanityApp fallback={<Loading />}>
|
|
58
|
+
* <MyComponent />
|
|
59
|
+
* </SanityApp>
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export const SDKStudioContext = createContext<StudioWorkspaceHandle | null>(null)
|
|
65
|
+
SDKStudioContext.displayName = 'SDKStudioContext'
|