@sanity/sdk-react 2.2.0 → 2.3.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/dist/index.d.ts +2 -3
- package/dist/index.js +101 -26
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/components/auth/AuthBoundary.test.tsx +33 -20
- package/src/components/auth/AuthBoundary.tsx +20 -5
- package/src/components/auth/LoginError.tsx +9 -12
- package/src/components/errors/CorsErrorComponent.test.tsx +48 -0
- package/src/components/errors/CorsErrorComponent.tsx +37 -0
- package/src/components/errors/Error.styles.ts +35 -0
- package/src/components/errors/Error.tsx +40 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +87 -38
- package/src/context/ComlinkTokenRefresh.tsx +2 -1
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +16 -7
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +56 -14
- package/src/hooks/dashboard/{useManageFavorite.test.ts → useManageFavorite.test.tsx} +99 -44
- package/src/hooks/document/{useDocument.test.ts → useDocument.test.tsx} +25 -22
- package/src/hooks/document/{useDocumentEvent.test.ts → useDocumentEvent.test.tsx} +17 -16
- package/src/hooks/document/{useDocumentPermissions.test.ts → useDocumentPermissions.test.tsx} +101 -40
- package/src/hooks/document/{useEditDocument.test.ts → useEditDocument.test.tsx} +52 -22
- package/src/hooks/documents/useDocuments.test.tsx +63 -25
- package/src/hooks/helpers/createCallbackHook.test.tsx +41 -37
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +2 -2
- package/src/hooks/presence/usePresence.test.tsx +9 -6
- package/src/hooks/preview/useDocumentPreview.test.tsx +15 -16
- package/src/hooks/projection/useDocumentProjection.test.tsx +23 -38
- package/src/hooks/projection/useDocumentProjection.ts +3 -8
- package/src/hooks/query/useQuery.test.tsx +18 -10
- package/src/hooks/releases/useActiveReleases.test.tsx +25 -21
- package/src/hooks/releases/usePerspective.test.tsx +16 -22
- package/src/hooks/users/useUser.test.tsx +32 -15
- package/src/hooks/users/useUsers.test.tsx +19 -11
- package/src/hooks/_synchronous-groq-js.mjs +0 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"browserslist": "extends @sanity/browserslist-config",
|
|
43
43
|
"prettier": "@sanity/prettier-config",
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@sanity/client": "^7.
|
|
45
|
+
"@sanity/client": "^7.12.0",
|
|
46
46
|
"@sanity/message-protocol": "^0.12.0",
|
|
47
47
|
"@sanity/types": "^3.83.0",
|
|
48
48
|
"@types/lodash-es": "^4.17.12",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"react-compiler-runtime": "19.1.0-rc.2",
|
|
52
52
|
"react-error-boundary": "^5.0.0",
|
|
53
53
|
"rxjs": "^7.8.2",
|
|
54
|
-
"@sanity/sdk": "2.
|
|
54
|
+
"@sanity/sdk": "2.3.1"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@sanity/browserslist-config": "^1.0.5",
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"@vitest/coverage-v8": "3.1.2",
|
|
67
67
|
"babel-plugin-react-compiler": "19.1.0-rc.1",
|
|
68
68
|
"eslint": "^9.22.0",
|
|
69
|
+
"groq-js": "^1.19.0",
|
|
69
70
|
"jsdom": "^25.0.1",
|
|
70
71
|
"prettier": "^3.5.3",
|
|
71
72
|
"react": "^19.1.0",
|
|
@@ -75,10 +76,10 @@
|
|
|
75
76
|
"vite": "^6.3.4",
|
|
76
77
|
"vitest": "^3.1.2",
|
|
77
78
|
"@repo/config-eslint": "0.0.0",
|
|
78
|
-
"@repo/config-test": "0.0.1",
|
|
79
79
|
"@repo/package.bundle": "3.82.0",
|
|
80
|
-
"@repo/
|
|
81
|
-
"@repo/package.config": "0.0.1"
|
|
80
|
+
"@repo/config-test": "0.0.1",
|
|
81
|
+
"@repo/package.config": "0.0.1",
|
|
82
|
+
"@repo/tsconfig": "0.0.1"
|
|
82
83
|
},
|
|
83
84
|
"peerDependencies": {
|
|
84
85
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -8,7 +8,6 @@ import {ResourceProvider} from '../../context/ResourceProvider'
|
|
|
8
8
|
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
9
9
|
import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
|
|
10
10
|
import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
|
|
11
|
-
import {useSanityInstance} from '../../hooks/context/useSanityInstance'
|
|
12
11
|
import {AuthBoundary} from './AuthBoundary'
|
|
13
12
|
|
|
14
13
|
// Mock hooks
|
|
@@ -23,9 +22,6 @@ vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
|
23
22
|
vi.mock('../../hooks/auth/useLogOut', () => ({
|
|
24
23
|
useLogOut: vi.fn(() => async () => {}),
|
|
25
24
|
}))
|
|
26
|
-
vi.mock('../../hooks/context/useSanityInstance', () => ({
|
|
27
|
-
useSanityInstance: vi.fn(),
|
|
28
|
-
}))
|
|
29
25
|
|
|
30
26
|
// Mock AuthError throwing scenario
|
|
31
27
|
vi.mock('./AuthError', async (importOriginal) => {
|
|
@@ -109,7 +105,6 @@ describe('AuthBoundary', () => {
|
|
|
109
105
|
const mockUseAuthState = vi.mocked(useAuthState)
|
|
110
106
|
const mockUseLoginUrl = vi.mocked(useLoginUrl)
|
|
111
107
|
const mockUseVerifyOrgProjects = vi.mocked(useVerifyOrgProjects)
|
|
112
|
-
const mockUseSanityInstance = vi.mocked(useSanityInstance)
|
|
113
108
|
const testProjectIds = ['proj-test'] // Example project ID for tests
|
|
114
109
|
|
|
115
110
|
// Mock Sanity instance
|
|
@@ -139,8 +134,6 @@ describe('AuthBoundary', () => {
|
|
|
139
134
|
mockUseLoginUrl.mockReturnValue('http://example.com/login')
|
|
140
135
|
// Default mock for useVerifyOrgProjects - returns null (no error)
|
|
141
136
|
mockUseVerifyOrgProjects.mockImplementation(() => null)
|
|
142
|
-
// Mock useSanityInstance to return our mock instance
|
|
143
|
-
mockUseSanityInstance.mockReturnValue(mockSanityInstance)
|
|
144
137
|
})
|
|
145
138
|
|
|
146
139
|
afterEach(() => {
|
|
@@ -170,7 +163,9 @@ describe('AuthBoundary', () => {
|
|
|
170
163
|
isExchangingToken: false,
|
|
171
164
|
})
|
|
172
165
|
const {container} = render(
|
|
173
|
-
<
|
|
166
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
167
|
+
<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
|
|
168
|
+
</ResourceProvider>,
|
|
174
169
|
)
|
|
175
170
|
|
|
176
171
|
// The callback screen renders null check that it renders nothing
|
|
@@ -184,7 +179,11 @@ describe('AuthBoundary', () => {
|
|
|
184
179
|
currentUser: null,
|
|
185
180
|
token: 'exampleToken',
|
|
186
181
|
})
|
|
187
|
-
render(
|
|
182
|
+
render(
|
|
183
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
184
|
+
<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
|
|
185
|
+
</ResourceProvider>,
|
|
186
|
+
)
|
|
188
187
|
|
|
189
188
|
expect(screen.getByText('Protected Content')).toBeInTheDocument()
|
|
190
189
|
})
|
|
@@ -194,7 +193,11 @@ describe('AuthBoundary', () => {
|
|
|
194
193
|
type: AuthStateType.ERROR,
|
|
195
194
|
error: new Error('test error'),
|
|
196
195
|
})
|
|
197
|
-
render(
|
|
196
|
+
render(
|
|
197
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
198
|
+
<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
|
|
199
|
+
</ResourceProvider>,
|
|
200
|
+
)
|
|
198
201
|
|
|
199
202
|
// The AuthBoundary should throw an AuthError internally
|
|
200
203
|
// and then display the LoginError component as the fallback.
|
|
@@ -207,7 +210,11 @@ describe('AuthBoundary', () => {
|
|
|
207
210
|
})
|
|
208
211
|
|
|
209
212
|
it('renders children when logged in and org verification passes', () => {
|
|
210
|
-
render(
|
|
213
|
+
render(
|
|
214
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
215
|
+
<AuthBoundary projectIds={testProjectIds}>Protected Content</AuthBoundary>
|
|
216
|
+
</ResourceProvider>,
|
|
217
|
+
)
|
|
211
218
|
expect(screen.getByText('Protected Content')).toBeInTheDocument()
|
|
212
219
|
})
|
|
213
220
|
|
|
@@ -226,9 +233,11 @@ describe('AuthBoundary', () => {
|
|
|
226
233
|
|
|
227
234
|
// Need to catch the error thrown during render. ErrorBoundary mock handles this.
|
|
228
235
|
render(
|
|
229
|
-
<
|
|
230
|
-
<
|
|
231
|
-
|
|
236
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
237
|
+
<AuthBoundary verifyOrganization={true} projectIds={testProjectIds}>
|
|
238
|
+
<div>Protected Content</div>
|
|
239
|
+
</AuthBoundary>
|
|
240
|
+
</ResourceProvider>,
|
|
232
241
|
)
|
|
233
242
|
|
|
234
243
|
// The ErrorBoundary's FallbackComponent should be rendered
|
|
@@ -256,9 +265,11 @@ describe('AuthBoundary', () => {
|
|
|
256
265
|
})
|
|
257
266
|
|
|
258
267
|
render(
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
|
|
268
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
269
|
+
<AuthBoundary verifyOrganization={false} projectIds={testProjectIds}>
|
|
270
|
+
<div>Protected Content</div>
|
|
271
|
+
</AuthBoundary>
|
|
272
|
+
</ResourceProvider>,
|
|
262
273
|
)
|
|
263
274
|
|
|
264
275
|
// Should render children because verification is disabled
|
|
@@ -279,9 +290,11 @@ describe('AuthBoundary', () => {
|
|
|
279
290
|
mockUseVerifyOrgProjects.mockImplementation(() => null)
|
|
280
291
|
|
|
281
292
|
render(
|
|
282
|
-
<
|
|
283
|
-
<
|
|
284
|
-
|
|
293
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
294
|
+
<AuthBoundary projectIds={testProjectIds}>
|
|
295
|
+
<div>Protected Content</div>
|
|
296
|
+
</AuthBoundary>
|
|
297
|
+
</ResourceProvider>,
|
|
285
298
|
)
|
|
286
299
|
|
|
287
300
|
await waitFor(() => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {CorsOriginError} from '@sanity/client'
|
|
2
|
+
import {AuthStateType, getCorsErrorProjectId} from '@sanity/sdk'
|
|
2
3
|
import {useEffect, useMemo} from 'react'
|
|
3
4
|
import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
|
|
4
5
|
|
|
@@ -6,6 +7,8 @@ import {ComlinkTokenRefreshProvider} from '../../context/ComlinkTokenRefresh'
|
|
|
6
7
|
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
7
8
|
import {useLoginUrl} from '../../hooks/auth/useLoginUrl'
|
|
8
9
|
import {useVerifyOrgProjects} from '../../hooks/auth/useVerifyOrgProjects'
|
|
10
|
+
import {useSanityInstance} from '../../hooks/context/useSanityInstance'
|
|
11
|
+
import {CorsErrorComponent} from '../errors/CorsErrorComponent'
|
|
9
12
|
import {isInIframe} from '../utils'
|
|
10
13
|
import {AuthError} from './AuthError'
|
|
11
14
|
import {ConfigurationError} from './ConfigurationError'
|
|
@@ -107,6 +110,14 @@ export function AuthBoundary({
|
|
|
107
110
|
}: AuthBoundaryProps): React.ReactNode {
|
|
108
111
|
const FallbackComponent = useMemo(() => {
|
|
109
112
|
return function LoginComponentWithLayoutProps(fallbackProps: FallbackProps) {
|
|
113
|
+
if (fallbackProps.error instanceof CorsOriginError) {
|
|
114
|
+
return (
|
|
115
|
+
<CorsErrorComponent
|
|
116
|
+
{...fallbackProps}
|
|
117
|
+
projectId={getCorsErrorProjectId(fallbackProps.error)}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
110
121
|
return <LoginErrorComponent {...fallbackProps} />
|
|
111
122
|
}
|
|
112
123
|
}, [LoginErrorComponent])
|
|
@@ -144,17 +155,21 @@ function AuthSwitch({
|
|
|
144
155
|
...props
|
|
145
156
|
}: AuthSwitchProps) {
|
|
146
157
|
const authState = useAuthState()
|
|
147
|
-
const
|
|
158
|
+
const instance = useSanityInstance()
|
|
159
|
+
const studioModeEnabled = instance.config.studioMode?.enabled
|
|
160
|
+
const disableVerifyOrg =
|
|
161
|
+
!verifyOrganization || studioModeEnabled || authState.type !== AuthStateType.LOGGED_IN
|
|
162
|
+
const orgError = useVerifyOrgProjects(disableVerifyOrg, projectIds)
|
|
148
163
|
|
|
149
164
|
const isLoggedOut = authState.type === AuthStateType.LOGGED_OUT && !authState.isDestroyingSession
|
|
150
165
|
const loginUrl = useLoginUrl()
|
|
151
166
|
|
|
152
167
|
useEffect(() => {
|
|
153
|
-
if (isLoggedOut && !isInIframe()) {
|
|
154
|
-
// We don't want to redirect to login if we're in the Dashboard
|
|
168
|
+
if (isLoggedOut && !isInIframe() && !studioModeEnabled) {
|
|
169
|
+
// We don't want to redirect to login if we're in the Dashboard nor in studio mode
|
|
155
170
|
window.location.href = loginUrl
|
|
156
171
|
}
|
|
157
|
-
}, [isLoggedOut, loginUrl])
|
|
172
|
+
}, [isLoggedOut, loginUrl, studioModeEnabled])
|
|
158
173
|
|
|
159
174
|
// Only check the error if verification is enabled
|
|
160
175
|
if (verifyOrganization && orgError) {
|
|
@@ -5,6 +5,7 @@ import {type FallbackProps} from 'react-error-boundary'
|
|
|
5
5
|
|
|
6
6
|
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
7
7
|
import {useLogOut} from '../../hooks/auth/useLogOut'
|
|
8
|
+
import {Error} from '../errors/Error'
|
|
8
9
|
import {AuthError} from './AuthError'
|
|
9
10
|
import {ConfigurationError} from './ConfigurationError'
|
|
10
11
|
/**
|
|
@@ -59,17 +60,13 @@ export function LoginError({error, resetErrorBoundary}: LoginErrorProps): React.
|
|
|
59
60
|
}, [authState, handleRetry, error])
|
|
60
61
|
|
|
61
62
|
return (
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<button className="sc-login-error__button" onClick={handleRetry}>
|
|
71
|
-
Retry
|
|
72
|
-
</button>
|
|
73
|
-
</div>
|
|
63
|
+
<Error
|
|
64
|
+
heading={error instanceof AuthError ? 'Authentication Error' : 'Configuration Error'}
|
|
65
|
+
description={authErrorMessage}
|
|
66
|
+
cta={{
|
|
67
|
+
text: 'Retry',
|
|
68
|
+
onClick: handleRetry,
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
74
71
|
)
|
|
75
72
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {render, screen} from '../../../test/test-utils'
|
|
4
|
+
import {CorsErrorComponent} from './CorsErrorComponent'
|
|
5
|
+
|
|
6
|
+
describe('CorsErrorComponent', () => {
|
|
7
|
+
it('shows origin and manage link when projectId is provided', () => {
|
|
8
|
+
const origin = 'https://example.com'
|
|
9
|
+
const originalLocation = window.location
|
|
10
|
+
// Redefine window.location to control origin in this test
|
|
11
|
+
Object.defineProperty(window, 'location', {
|
|
12
|
+
value: {origin},
|
|
13
|
+
configurable: true,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
render(
|
|
17
|
+
<CorsErrorComponent
|
|
18
|
+
projectId="proj123"
|
|
19
|
+
error={new Error('nope')}
|
|
20
|
+
resetErrorBoundary={() => {}}
|
|
21
|
+
/>,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
expect(screen.getByText('Before you continue…')).toBeInTheDocument()
|
|
25
|
+
expect(screen.getByText(origin)).toBeInTheDocument()
|
|
26
|
+
|
|
27
|
+
const link = screen.getByRole('link', {name: 'Manage CORS configuration'}) as HTMLAnchorElement
|
|
28
|
+
expect(link).toBeInTheDocument()
|
|
29
|
+
expect(link.target).toBe('_blank')
|
|
30
|
+
expect(link.rel).toContain('noopener')
|
|
31
|
+
expect(link.href).toContain('https://sanity.io/manage/project/proj123/api')
|
|
32
|
+
expect(link.href).toContain('cors=add')
|
|
33
|
+
expect(link.href).toContain(`origin=${encodeURIComponent(origin)}`)
|
|
34
|
+
expect(link.href).toContain('credentials=include')
|
|
35
|
+
|
|
36
|
+
// restore
|
|
37
|
+
Object.defineProperty(window, 'location', {value: originalLocation})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('shows error message when projectId is null', () => {
|
|
41
|
+
const error = new Error('some error message')
|
|
42
|
+
render(<CorsErrorComponent projectId={null} error={error} resetErrorBoundary={() => {}} />)
|
|
43
|
+
|
|
44
|
+
expect(screen.getByText('Before you continue…')).toBeInTheDocument()
|
|
45
|
+
expect(screen.getByText('some error message')).toBeInTheDocument()
|
|
46
|
+
expect(screen.queryByRole('link', {name: 'Manage CORS configuration'})).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {useMemo} from 'react'
|
|
2
|
+
import {type FallbackProps} from 'react-error-boundary'
|
|
3
|
+
|
|
4
|
+
import {Error} from './Error'
|
|
5
|
+
|
|
6
|
+
type CorsErrorComponentProps = FallbackProps & {
|
|
7
|
+
projectId: string | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CorsErrorComponent({projectId, error}: CorsErrorComponentProps): React.ReactNode {
|
|
11
|
+
const origin = window.location.origin
|
|
12
|
+
const corsUrl = useMemo(() => {
|
|
13
|
+
const url = new URL(`https://sanity.io/manage/project/${projectId}/api`)
|
|
14
|
+
url.searchParams.set('cors', 'add')
|
|
15
|
+
url.searchParams.set('origin', origin)
|
|
16
|
+
url.searchParams.set('credentials', 'include')
|
|
17
|
+
return url.toString()
|
|
18
|
+
}, [origin, projectId])
|
|
19
|
+
return (
|
|
20
|
+
<Error
|
|
21
|
+
heading="Before you continue…"
|
|
22
|
+
{...(projectId
|
|
23
|
+
? {
|
|
24
|
+
description:
|
|
25
|
+
'To access your content, you need to <strong>add the following URL as a CORS origin</strong> to your Sanity project.',
|
|
26
|
+
code: origin,
|
|
27
|
+
cta: {
|
|
28
|
+
text: 'Manage CORS configuration',
|
|
29
|
+
href: corsUrl,
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
: {
|
|
33
|
+
description: error?.message,
|
|
34
|
+
})}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const FONT_SANS_SERIF = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Helvetica, Arial, system-ui, sans-serif`
|
|
2
|
+
const FONT_MONOSPACE = `-apple-system-ui-monospace, 'SF Mono', Menlo, Monaco, Consolas, monospace`
|
|
3
|
+
|
|
4
|
+
const styles: Record<string, React.CSSProperties> = {
|
|
5
|
+
container: {
|
|
6
|
+
padding: '28px',
|
|
7
|
+
fontFamily: FONT_SANS_SERIF,
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
gap: '21px',
|
|
11
|
+
fontSize: '14px',
|
|
12
|
+
},
|
|
13
|
+
heading: {
|
|
14
|
+
margin: 0,
|
|
15
|
+
fontSize: '28px',
|
|
16
|
+
fontWeight: 700,
|
|
17
|
+
},
|
|
18
|
+
paragraph: {
|
|
19
|
+
margin: 0,
|
|
20
|
+
},
|
|
21
|
+
link: {
|
|
22
|
+
appearance: 'none',
|
|
23
|
+
background: 'transparent',
|
|
24
|
+
border: 0,
|
|
25
|
+
padding: 0,
|
|
26
|
+
font: 'inherit',
|
|
27
|
+
textDecoration: 'underline',
|
|
28
|
+
cursor: 'pointer',
|
|
29
|
+
},
|
|
30
|
+
code: {
|
|
31
|
+
fontFamily: FONT_MONOSPACE,
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default styles
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import styles from './Error.styles'
|
|
2
|
+
|
|
3
|
+
type ErrorProps = {
|
|
4
|
+
heading: string
|
|
5
|
+
description?: string
|
|
6
|
+
code?: string
|
|
7
|
+
cta?: {
|
|
8
|
+
text: string
|
|
9
|
+
href?: string
|
|
10
|
+
onClick?: () => void
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Error({heading, description, code, cta}: ErrorProps): React.ReactNode {
|
|
15
|
+
return (
|
|
16
|
+
<div style={styles['container']}>
|
|
17
|
+
<h1 style={styles['heading']}>{heading}</h1>
|
|
18
|
+
|
|
19
|
+
{description && (
|
|
20
|
+
<p style={styles['paragraph']} dangerouslySetInnerHTML={{__html: description}} />
|
|
21
|
+
)}
|
|
22
|
+
|
|
23
|
+
{code && <code style={styles['code']}>{code}</code>}
|
|
24
|
+
|
|
25
|
+
{cta && (cta.href || cta.onClick) && (
|
|
26
|
+
<p style={styles['paragraph']}>
|
|
27
|
+
{cta.href ? (
|
|
28
|
+
<a style={styles['link']} href={cta.href} target="_blank" rel="noopener noreferrer">
|
|
29
|
+
{cta.text}
|
|
30
|
+
</a>
|
|
31
|
+
) : (
|
|
32
|
+
<button style={styles['link']} onClick={cta.onClick}>
|
|
33
|
+
{cta.text}
|
|
34
|
+
</button>
|
|
35
|
+
)}
|
|
36
|
+
</p>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -7,6 +7,7 @@ import {useAuthState} from '../hooks/auth/useAuthState'
|
|
|
7
7
|
import {useWindowConnection} from '../hooks/comlink/useWindowConnection'
|
|
8
8
|
import {useSanityInstance} from '../hooks/context/useSanityInstance'
|
|
9
9
|
import {ComlinkTokenRefreshProvider} from './ComlinkTokenRefresh'
|
|
10
|
+
import {ResourceProvider} from './ResourceProvider'
|
|
10
11
|
|
|
11
12
|
// Mocks
|
|
12
13
|
vi.mock('@sanity/sdk', async () => {
|
|
@@ -35,11 +36,15 @@ const mockGetIsInDashboardState = getIsInDashboardState as Mock
|
|
|
35
36
|
const mockSetAuthToken = setAuthToken as Mock
|
|
36
37
|
const mockUseAuthState = useAuthState as Mock
|
|
37
38
|
const mockUseWindowConnection = useWindowConnection as Mock
|
|
38
|
-
const mockUseSanityInstance = useSanityInstance as Mock
|
|
39
|
+
const mockUseSanityInstance = useSanityInstance as unknown as Mock
|
|
39
40
|
|
|
40
41
|
const mockFetch = vi.fn()
|
|
41
42
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
-
const mockSanityInstance: any = {
|
|
43
|
+
const mockSanityInstance: any = {
|
|
44
|
+
projectId: 'test',
|
|
45
|
+
dataset: 'test',
|
|
46
|
+
config: {studioMode: {enabled: false}},
|
|
47
|
+
}
|
|
43
48
|
|
|
44
49
|
describe('ComlinkTokenRefresh', () => {
|
|
45
50
|
beforeEach(() => {
|
|
@@ -64,9 +69,11 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
64
69
|
it('should not request new token on 401 if not in dashboard', async () => {
|
|
65
70
|
mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
|
|
66
71
|
const {rerender} = render(
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
72
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
73
|
+
<ComlinkTokenRefreshProvider>
|
|
74
|
+
<div>Test</div>
|
|
75
|
+
</ComlinkTokenRefreshProvider>
|
|
76
|
+
</ResourceProvider>,
|
|
70
77
|
)
|
|
71
78
|
|
|
72
79
|
mockUseAuthState.mockReturnValue({
|
|
@@ -75,9 +82,11 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
75
82
|
})
|
|
76
83
|
act(() => {
|
|
77
84
|
rerender(
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
85
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
86
|
+
<ComlinkTokenRefreshProvider>
|
|
87
|
+
<div>Test</div>
|
|
88
|
+
</ComlinkTokenRefreshProvider>
|
|
89
|
+
</ResourceProvider>,
|
|
81
90
|
)
|
|
82
91
|
})
|
|
83
92
|
|
|
@@ -93,11 +102,14 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
93
102
|
mockGetIsInDashboardState.mockReturnValue({getCurrent: () => true})
|
|
94
103
|
})
|
|
95
104
|
|
|
96
|
-
it('should initialize useWindowConnection with correct parameters', () => {
|
|
105
|
+
it('should initialize useWindowConnection with correct parameters when not in studio mode', () => {
|
|
106
|
+
// Simulate studio mode disabled by default
|
|
97
107
|
render(
|
|
98
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
108
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
109
|
+
<ComlinkTokenRefreshProvider>
|
|
110
|
+
<div>Test</div>
|
|
111
|
+
</ComlinkTokenRefreshProvider>
|
|
112
|
+
</ResourceProvider>,
|
|
101
113
|
)
|
|
102
114
|
|
|
103
115
|
expect(mockUseWindowConnection).toHaveBeenCalledWith(
|
|
@@ -108,7 +120,7 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
108
120
|
)
|
|
109
121
|
})
|
|
110
122
|
|
|
111
|
-
it('should handle received token', async () => {
|
|
123
|
+
it('should handle received token when not in studio mode', async () => {
|
|
112
124
|
mockUseAuthState.mockReturnValue({
|
|
113
125
|
type: AuthStateType.ERROR,
|
|
114
126
|
error: {statusCode: 401, message: 'Unauthorized'},
|
|
@@ -116,20 +128,22 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
116
128
|
mockFetch.mockResolvedValueOnce({token: 'new-token'})
|
|
117
129
|
|
|
118
130
|
render(
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
131
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
132
|
+
<ComlinkTokenRefreshProvider>
|
|
133
|
+
<div>Test</div>
|
|
134
|
+
</ComlinkTokenRefreshProvider>
|
|
135
|
+
</ResourceProvider>,
|
|
122
136
|
)
|
|
123
137
|
|
|
124
138
|
await act(async () => {
|
|
125
139
|
await vi.advanceTimersByTimeAsync(100)
|
|
126
140
|
})
|
|
127
141
|
|
|
128
|
-
expect(mockSetAuthToken).toHaveBeenCalledWith(
|
|
142
|
+
expect(mockSetAuthToken).toHaveBeenCalledWith(expect.any(Object), 'new-token')
|
|
129
143
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
130
144
|
})
|
|
131
145
|
|
|
132
|
-
it('should not set auth token if received token is null', async () => {
|
|
146
|
+
it('should not set auth token if received token is null when not in studio mode', async () => {
|
|
133
147
|
mockUseAuthState.mockReturnValue({
|
|
134
148
|
type: AuthStateType.ERROR,
|
|
135
149
|
error: {statusCode: 401, message: 'Unauthorized'},
|
|
@@ -137,9 +151,11 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
137
151
|
mockFetch.mockResolvedValueOnce({token: null})
|
|
138
152
|
|
|
139
153
|
render(
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
|
|
154
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
155
|
+
<ComlinkTokenRefreshProvider>
|
|
156
|
+
<div>Test</div>
|
|
157
|
+
</ComlinkTokenRefreshProvider>
|
|
158
|
+
</ResourceProvider>,
|
|
143
159
|
)
|
|
144
160
|
|
|
145
161
|
await act(async () => {
|
|
@@ -149,7 +165,7 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
149
165
|
expect(mockSetAuthToken).not.toHaveBeenCalled()
|
|
150
166
|
})
|
|
151
167
|
|
|
152
|
-
it('should handle fetch errors gracefully', async () => {
|
|
168
|
+
it('should handle fetch errors gracefully when not in studio mode', async () => {
|
|
153
169
|
mockUseAuthState.mockReturnValue({
|
|
154
170
|
type: AuthStateType.ERROR,
|
|
155
171
|
error: {statusCode: 401, message: 'Unauthorized'},
|
|
@@ -157,9 +173,11 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
157
173
|
mockFetch.mockRejectedValueOnce(new Error('Fetch failed'))
|
|
158
174
|
|
|
159
175
|
render(
|
|
160
|
-
<
|
|
161
|
-
<
|
|
162
|
-
|
|
176
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
177
|
+
<ComlinkTokenRefreshProvider>
|
|
178
|
+
<div>Test</div>
|
|
179
|
+
</ComlinkTokenRefreshProvider>
|
|
180
|
+
</ResourceProvider>,
|
|
163
181
|
)
|
|
164
182
|
|
|
165
183
|
await act(async () => {
|
|
@@ -170,12 +188,15 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
170
188
|
})
|
|
171
189
|
|
|
172
190
|
describe('Automatic token refresh', () => {
|
|
173
|
-
it('should not request new token for non-401 errors', async () => {
|
|
191
|
+
it('should not request new token for non-401 errors when not in studio mode', async () => {
|
|
174
192
|
mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
|
|
175
193
|
const {rerender} = render(
|
|
176
|
-
<
|
|
177
|
-
<
|
|
178
|
-
|
|
194
|
+
<ResourceProvider fallback={null}>
|
|
195
|
+
<ComlinkTokenRefreshProvider>
|
|
196
|
+
<div>Test</div>
|
|
197
|
+
</ComlinkTokenRefreshProvider>
|
|
198
|
+
,
|
|
199
|
+
</ResourceProvider>,
|
|
179
200
|
)
|
|
180
201
|
|
|
181
202
|
mockUseAuthState.mockReturnValue({
|
|
@@ -184,9 +205,11 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
184
205
|
})
|
|
185
206
|
act(() => {
|
|
186
207
|
rerender(
|
|
187
|
-
<
|
|
188
|
-
<
|
|
189
|
-
|
|
208
|
+
<ResourceProvider fallback={null}>
|
|
209
|
+
<ComlinkTokenRefreshProvider>
|
|
210
|
+
<div>Test</div>
|
|
211
|
+
</ComlinkTokenRefreshProvider>
|
|
212
|
+
</ResourceProvider>,
|
|
190
213
|
)
|
|
191
214
|
})
|
|
192
215
|
|
|
@@ -196,24 +219,50 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
196
219
|
expect(mockFetch).not.toHaveBeenCalled()
|
|
197
220
|
})
|
|
198
221
|
|
|
199
|
-
it('should request new token on LOGGED_OUT state', async () => {
|
|
222
|
+
it('should request new token on LOGGED_OUT state when not in studio mode', async () => {
|
|
200
223
|
mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
|
|
201
224
|
const {rerender} = render(
|
|
202
|
-
<
|
|
203
|
-
<
|
|
204
|
-
|
|
225
|
+
<ResourceProvider fallback={null}>
|
|
226
|
+
<ComlinkTokenRefreshProvider>
|
|
227
|
+
<div>Test</div>
|
|
228
|
+
</ComlinkTokenRefreshProvider>
|
|
229
|
+
</ResourceProvider>,
|
|
205
230
|
)
|
|
206
231
|
|
|
207
232
|
mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_OUT})
|
|
208
233
|
act(() => {
|
|
209
234
|
rerender(
|
|
235
|
+
<ResourceProvider fallback={null}>
|
|
236
|
+
<ComlinkTokenRefreshProvider>
|
|
237
|
+
<div>Test</div>
|
|
238
|
+
</ComlinkTokenRefreshProvider>
|
|
239
|
+
</ResourceProvider>,
|
|
240
|
+
)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
expect(mockFetch).toHaveBeenCalledWith('dashboard/v1/auth/tokens/create')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
describe('when in studio mode', () => {
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
// Make the instance report studio mode enabled
|
|
249
|
+
mockUseSanityInstance.mockReturnValue({
|
|
250
|
+
...mockSanityInstance,
|
|
251
|
+
config: {studioMode: {enabled: true}},
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('should not render DashboardTokenRefresh when studio mode enabled', () => {
|
|
256
|
+
render(
|
|
210
257
|
<ComlinkTokenRefreshProvider>
|
|
211
258
|
<div>Test</div>
|
|
212
259
|
</ComlinkTokenRefreshProvider>,
|
|
213
260
|
)
|
|
214
|
-
})
|
|
215
261
|
|
|
216
|
-
|
|
262
|
+
// In studio mode, provider should return children directly
|
|
263
|
+
// So window connection should not be initialized
|
|
264
|
+
expect(mockUseWindowConnection).not.toHaveBeenCalled()
|
|
265
|
+
})
|
|
217
266
|
})
|
|
218
267
|
})
|
|
219
268
|
})
|