@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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {Suspense} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props for the SanityInstanceProvider component
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export interface SanityInstanceProviderProps {
|
|
11
|
+
/**
|
|
12
|
+
* A pre-created SanityInstance to provide to child components.
|
|
13
|
+
* The caller owns the instance lifecycle — SanityInstanceProvider
|
|
14
|
+
* will not dispose it on unmount.
|
|
15
|
+
*/
|
|
16
|
+
instance: SanityInstance
|
|
17
|
+
/**
|
|
18
|
+
* React node to show while content is loading.
|
|
19
|
+
* Used as the fallback for the internal Suspense boundary.
|
|
20
|
+
*/
|
|
21
|
+
fallback: React.ReactNode
|
|
22
|
+
children: React.ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provides an externally-created Sanity instance to child components through React Context.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* Unlike {@link ResourceProvider}, this component does not create or dispose a SanityInstance.
|
|
32
|
+
* The caller is responsible for creating the instance via `createSanityInstance` and disposing
|
|
33
|
+
* it when appropriate. This is useful when a non-React system layer (e.g. a state machine)
|
|
34
|
+
* owns the instance and the React tree should consume it without managing its lifecycle.
|
|
35
|
+
*
|
|
36
|
+
* All SDK hooks (`useSanityInstance`, `useDocuments`, etc.) will read from the provided instance.
|
|
37
|
+
*
|
|
38
|
+
* @example Providing a pre-created instance
|
|
39
|
+
* ```tsx
|
|
40
|
+
* import { createSanityInstance, type SanityConfig } from '@sanity/sdk'
|
|
41
|
+
* import { SanityInstanceProvider } from '@sanity/sdk-react'
|
|
42
|
+
*
|
|
43
|
+
* const config: SanityConfig = {
|
|
44
|
+
* projectId: 'my-project-id',
|
|
45
|
+
* dataset: 'production',
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* const instance = createSanityInstance(config)
|
|
49
|
+
*
|
|
50
|
+
* function App() {
|
|
51
|
+
* return (
|
|
52
|
+
* <SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
|
|
53
|
+
* <MyApp />
|
|
54
|
+
* </SanityInstanceProvider>
|
|
55
|
+
* )
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @category Components
|
|
60
|
+
*/
|
|
61
|
+
export function SanityInstanceProvider({
|
|
62
|
+
instance,
|
|
63
|
+
fallback,
|
|
64
|
+
children,
|
|
65
|
+
}: SanityInstanceProviderProps): React.ReactNode {
|
|
66
|
+
return (
|
|
67
|
+
<SanityInstanceContext.Provider value={instance}>
|
|
68
|
+
<Suspense fallback={fallback}>{children}</Suspense>
|
|
69
|
+
</SanityInstanceContext.Provider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {AuthStateType, setAuthToken} from '@sanity/sdk'
|
|
2
|
+
import {act, render} from '@testing-library/react'
|
|
3
|
+
import {of} from 'rxjs'
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {useAuthState} from '../hooks/auth/useAuthState'
|
|
7
|
+
import {ResourceProvider} from './ResourceProvider'
|
|
8
|
+
import {
|
|
9
|
+
isWorkbenchEnvironment,
|
|
10
|
+
observeWorkbenchToken,
|
|
11
|
+
refreshWorkbenchToken,
|
|
12
|
+
} from './workbenchToken'
|
|
13
|
+
import {WorkbenchTokenRefreshProvider} from './WorkbenchTokenRefresh'
|
|
14
|
+
|
|
15
|
+
vi.mock('@sanity/sdk', async () => {
|
|
16
|
+
const actual = await vi.importActual('@sanity/sdk')
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
setAuthToken: vi.fn(),
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
vi.mock('../hooks/auth/useAuthState', () => ({
|
|
24
|
+
useAuthState: vi.fn(),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
vi.mock('./workbenchToken', () => ({
|
|
28
|
+
isWorkbenchEnvironment: vi.fn(() => false),
|
|
29
|
+
observeWorkbenchToken: vi.fn(() => undefined),
|
|
30
|
+
refreshWorkbenchToken: vi.fn(),
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
const mockSetAuthToken = setAuthToken as Mock
|
|
34
|
+
const mockUseAuthState = useAuthState as Mock
|
|
35
|
+
const mockIsWorkbenchEnvironment = isWorkbenchEnvironment as Mock
|
|
36
|
+
const mockObserveWorkbenchToken = observeWorkbenchToken as Mock
|
|
37
|
+
const mockRefreshWorkbenchToken = refreshWorkbenchToken as Mock
|
|
38
|
+
|
|
39
|
+
const renderProvider = () =>
|
|
40
|
+
render(
|
|
41
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
42
|
+
<WorkbenchTokenRefreshProvider>
|
|
43
|
+
<div>Test</div>
|
|
44
|
+
</WorkbenchTokenRefreshProvider>
|
|
45
|
+
</ResourceProvider>,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
describe('WorkbenchTokenRefreshProvider', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.clearAllMocks()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('when not in the workbench', () => {
|
|
58
|
+
it('does not subscribe to the OS token', () => {
|
|
59
|
+
mockIsWorkbenchEnvironment.mockReturnValue(false)
|
|
60
|
+
|
|
61
|
+
act(() => {
|
|
62
|
+
renderProvider()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(mockSetAuthToken).not.toHaveBeenCalled()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('when in the workbench', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockIsWorkbenchEnvironment.mockReturnValue(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('mirrors the OS token into the auth store', () => {
|
|
75
|
+
mockObserveWorkbenchToken.mockReturnValue(of('workbench-token'))
|
|
76
|
+
|
|
77
|
+
act(() => {
|
|
78
|
+
renderProvider()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(mockSetAuthToken).toHaveBeenCalledWith(expect.anything(), 'workbench-token')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('asks the OS to reissue the token on a 401', () => {
|
|
85
|
+
mockObserveWorkbenchToken.mockReturnValue(of('workbench-token'))
|
|
86
|
+
|
|
87
|
+
const {rerender} = renderProvider()
|
|
88
|
+
|
|
89
|
+
mockUseAuthState.mockReturnValue({
|
|
90
|
+
type: AuthStateType.ERROR,
|
|
91
|
+
error: {statusCode: 401, message: 'Unauthorized'},
|
|
92
|
+
})
|
|
93
|
+
act(() => {
|
|
94
|
+
rerender(
|
|
95
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
96
|
+
<WorkbenchTokenRefreshProvider>
|
|
97
|
+
<div>Test</div>
|
|
98
|
+
</WorkbenchTokenRefreshProvider>
|
|
99
|
+
</ResourceProvider>,
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(mockRefreshWorkbenchToken).toHaveBeenCalledTimes(1)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {type ClientError} from '@sanity/client'
|
|
2
|
+
import {AuthStateType, setAuthToken} from '@sanity/sdk'
|
|
3
|
+
import React, {type PropsWithChildren, useEffect, useRef} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useAuthState} from '../hooks/auth/useAuthState'
|
|
6
|
+
import {useSanityInstance} from '../hooks/context/useSanityInstance'
|
|
7
|
+
import {
|
|
8
|
+
isWorkbenchEnvironment,
|
|
9
|
+
observeWorkbenchToken,
|
|
10
|
+
refreshWorkbenchToken,
|
|
11
|
+
} from './workbenchToken'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Keeps the SDK auth token in sync with the workbench "OS".
|
|
15
|
+
*
|
|
16
|
+
* When running as a federated remote inside the workbench the OS owns the
|
|
17
|
+
* session, so we subscribe to its `auth.token` stream and mirror each value into
|
|
18
|
+
* the auth store — a token logs us in, `null` logs us out, and later OS
|
|
19
|
+
* sign-in/out propagates automatically. When a request is rejected with a 401
|
|
20
|
+
* (the token expired), we ask the OS to reissue rather than tearing the session
|
|
21
|
+
* down; the new token arrives back through the same subscription.
|
|
22
|
+
*/
|
|
23
|
+
function WorkbenchTokenRefresh({children}: PropsWithChildren) {
|
|
24
|
+
const instance = useSanityInstance()
|
|
25
|
+
const authState = useAuthState()
|
|
26
|
+
const processed401ErrorRef = useRef<unknown | null>(null)
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const token$ = observeWorkbenchToken()
|
|
30
|
+
if (!token$) return undefined
|
|
31
|
+
const subscription = token$.subscribe((token) => setAuthToken(instance, token))
|
|
32
|
+
return () => subscription.unsubscribe()
|
|
33
|
+
}, [instance])
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const has401Error =
|
|
37
|
+
authState.type === AuthStateType.ERROR && (authState.error as ClientError)?.statusCode === 401
|
|
38
|
+
|
|
39
|
+
if (has401Error && processed401ErrorRef.current !== authState.error) {
|
|
40
|
+
processed401ErrorRef.current = authState.error
|
|
41
|
+
refreshWorkbenchToken()
|
|
42
|
+
} else if (!has401Error) {
|
|
43
|
+
processed401ErrorRef.current = null
|
|
44
|
+
}
|
|
45
|
+
}, [authState])
|
|
46
|
+
|
|
47
|
+
return children
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Provides workbench OS token refresh. No-op outside the workbench, where the
|
|
52
|
+
* app uses its normal auth flow.
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
export const WorkbenchTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
|
|
56
|
+
if (isWorkbenchEnvironment()) {
|
|
57
|
+
return <WorkbenchTokenRefresh>{children}</WorkbenchTokenRefresh>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return children
|
|
61
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type SanityConfig} from '@sanity/sdk'
|
|
2
2
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
3
|
|
|
4
4
|
import {SanityApp} from '../components/SanityApp'
|
|
@@ -26,7 +26,6 @@ vi.mock('react-dom/client', () => ({
|
|
|
26
26
|
|
|
27
27
|
describe('renderSanityApp', () => {
|
|
28
28
|
let rootElement: HTMLElement | null
|
|
29
|
-
let namedResources: Record<string, DocumentResource>
|
|
30
29
|
|
|
31
30
|
beforeEach(() => {
|
|
32
31
|
vi.clearAllMocks()
|
|
@@ -35,9 +34,6 @@ describe('renderSanityApp', () => {
|
|
|
35
34
|
mockCreateRoot.mockClear()
|
|
36
35
|
rootElement = document.createElement('div')
|
|
37
36
|
document.body.appendChild(rootElement)
|
|
38
|
-
namedResources = {
|
|
39
|
-
default: {projectId: 'test-project', dataset: 'production'},
|
|
40
|
-
}
|
|
41
37
|
})
|
|
42
38
|
|
|
43
39
|
afterEach(() => {
|
|
@@ -48,44 +44,53 @@ describe('renderSanityApp', () => {
|
|
|
48
44
|
})
|
|
49
45
|
|
|
50
46
|
it('throws error when rootElement is null', () => {
|
|
51
|
-
|
|
47
|
+
const namedSources = {
|
|
48
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
expect(() => renderSanityApp(null, namedSources, {}, <div>Test</div>)).toThrowError(
|
|
52
52
|
'Missing root element to mount application into',
|
|
53
53
|
)
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
it('creates root with the provided element', () => {
|
|
57
|
-
|
|
57
|
+
const namedSources = {
|
|
58
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
58
62
|
|
|
59
63
|
expect(mockCreateRoot).toHaveBeenCalledWith(rootElement)
|
|
60
64
|
expect(mockCreateRoot).toHaveBeenCalledTimes(1)
|
|
61
65
|
})
|
|
62
66
|
|
|
63
|
-
it('
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
it('converts namedSources object to array of configs', () => {
|
|
68
|
+
const namedSources = {
|
|
69
|
+
main: {projectId: 'project-1', dataset: 'production'},
|
|
66
70
|
secondary: {projectId: 'project-2', dataset: 'staging'},
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
renderSanityApp(rootElement,
|
|
73
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
70
74
|
|
|
71
75
|
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
72
76
|
const renderCall = mockRender.mock.calls[0][0]
|
|
73
77
|
expect(renderCall).toBeDefined()
|
|
74
78
|
|
|
79
|
+
// The renderCall is the SanityApp component directly when not using StrictMode
|
|
75
80
|
expect(renderCall.type).toBe(SanityApp)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
default: {projectId: 'project-1', dataset: 'production'},
|
|
81
|
-
secondary: {projectId: 'project-2', dataset: 'staging'},
|
|
82
|
-
})
|
|
81
|
+
expect(renderCall.props.config).toEqual([
|
|
82
|
+
{projectId: 'project-1', dataset: 'production'},
|
|
83
|
+
{projectId: 'project-2', dataset: 'staging'},
|
|
84
|
+
])
|
|
83
85
|
})
|
|
84
86
|
|
|
85
87
|
it('renders without StrictMode when reactStrictMode is false', () => {
|
|
88
|
+
const namedSources = {
|
|
89
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
90
|
+
}
|
|
86
91
|
const children = <div>Test Children</div>
|
|
87
92
|
|
|
88
|
-
renderSanityApp(rootElement,
|
|
93
|
+
renderSanityApp(rootElement, namedSources, {reactStrictMode: false}, children)
|
|
89
94
|
|
|
90
95
|
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
91
96
|
const renderCall = mockRender.mock.calls[0][0]
|
|
@@ -96,9 +101,12 @@ describe('renderSanityApp', () => {
|
|
|
96
101
|
})
|
|
97
102
|
|
|
98
103
|
it('renders without StrictMode by default', () => {
|
|
104
|
+
const namedSources = {
|
|
105
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
106
|
+
}
|
|
99
107
|
const children = <div>Test Children</div>
|
|
100
108
|
|
|
101
|
-
renderSanityApp(rootElement,
|
|
109
|
+
renderSanityApp(rootElement, namedSources, {}, children)
|
|
102
110
|
|
|
103
111
|
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
104
112
|
const renderCall = mockRender.mock.calls[0][0]
|
|
@@ -109,14 +117,17 @@ describe('renderSanityApp', () => {
|
|
|
109
117
|
})
|
|
110
118
|
|
|
111
119
|
it('renders with StrictMode when reactStrictMode is true', () => {
|
|
120
|
+
const namedSources = {
|
|
121
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
122
|
+
}
|
|
112
123
|
const children = <div>Test Children</div>
|
|
113
124
|
|
|
114
|
-
renderSanityApp(rootElement,
|
|
125
|
+
renderSanityApp(rootElement, namedSources, {reactStrictMode: true}, children)
|
|
115
126
|
|
|
116
127
|
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
117
128
|
const renderCall = mockRender.mock.calls[0][0]
|
|
118
129
|
|
|
119
|
-
// Should have StrictMode wrapper (StrictMode is a Symbol)
|
|
130
|
+
// Should have StrictMode wrapper (StrictMode is a Symbol in React 18)
|
|
120
131
|
expect(renderCall.type).toBeDefined()
|
|
121
132
|
expect(renderCall.type.toString()).toContain('Symbol')
|
|
122
133
|
const strictModeChild = renderCall.props.children
|
|
@@ -125,7 +136,11 @@ describe('renderSanityApp', () => {
|
|
|
125
136
|
})
|
|
126
137
|
|
|
127
138
|
it('passes loading fallback to SanityApp', () => {
|
|
128
|
-
|
|
139
|
+
const namedSources = {
|
|
140
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
129
144
|
|
|
130
145
|
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
131
146
|
const renderCall = mockRender.mock.calls[0][0]
|
|
@@ -136,51 +151,103 @@ describe('renderSanityApp', () => {
|
|
|
136
151
|
})
|
|
137
152
|
|
|
138
153
|
it('returns an unmount function', () => {
|
|
139
|
-
const
|
|
154
|
+
const namedSources = {
|
|
155
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const unmount = renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
140
159
|
|
|
141
160
|
expect(typeof unmount).toBe('function')
|
|
142
161
|
})
|
|
143
162
|
|
|
144
163
|
it('calls root.unmount when unmount function is invoked', () => {
|
|
145
|
-
const
|
|
164
|
+
const namedSources = {
|
|
165
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const unmount = renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
146
169
|
|
|
147
170
|
expect(mockUnmount).not.toHaveBeenCalled()
|
|
148
171
|
unmount()
|
|
149
172
|
expect(mockUnmount).toHaveBeenCalledTimes(1)
|
|
150
173
|
})
|
|
151
174
|
|
|
152
|
-
it('handles empty
|
|
153
|
-
|
|
175
|
+
it('handles empty namedSources object', () => {
|
|
176
|
+
const namedSources = {}
|
|
154
177
|
|
|
155
|
-
renderSanityApp(rootElement,
|
|
178
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
156
179
|
|
|
157
180
|
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
158
181
|
const renderCall = mockRender.mock.calls[0][0]
|
|
159
182
|
const sanityAppElement = renderCall
|
|
160
183
|
|
|
161
184
|
expect(sanityAppElement.type).toBe(SanityApp)
|
|
162
|
-
expect(sanityAppElement.props.config).toEqual(
|
|
163
|
-
expect(sanityAppElement.props.resources).toEqual({})
|
|
185
|
+
expect(sanityAppElement.props.config).toEqual([])
|
|
164
186
|
})
|
|
165
187
|
|
|
166
188
|
it('handles single namedSource', () => {
|
|
167
|
-
const
|
|
168
|
-
|
|
189
|
+
const namedSources = {
|
|
190
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
169
191
|
}
|
|
170
|
-
|
|
192
|
+
|
|
193
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
171
194
|
|
|
172
195
|
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
173
196
|
const renderCall = mockRender.mock.calls[0][0]
|
|
174
197
|
const sanityAppElement = renderCall
|
|
175
198
|
|
|
176
199
|
expect(sanityAppElement.type).toBe(SanityApp)
|
|
177
|
-
expect(sanityAppElement.props.config).toEqual(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
200
|
+
expect(sanityAppElement.props.config).toEqual([
|
|
201
|
+
{projectId: 'test-project', dataset: 'production'},
|
|
202
|
+
])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('handles multiple namedSources', () => {
|
|
206
|
+
const namedSources = {
|
|
207
|
+
main: {projectId: 'project-1', dataset: 'production'},
|
|
208
|
+
blog: {projectId: 'project-2', dataset: 'staging'},
|
|
209
|
+
ecommerce: {projectId: 'project-3', dataset: 'development'},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
213
|
+
|
|
214
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
215
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
216
|
+
const sanityAppElement = renderCall
|
|
217
|
+
|
|
218
|
+
expect(sanityAppElement.type).toBe(SanityApp)
|
|
219
|
+
expect(sanityAppElement.props.config).toHaveLength(3)
|
|
220
|
+
expect(sanityAppElement.props.config).toEqual([
|
|
221
|
+
{projectId: 'project-1', dataset: 'production'},
|
|
222
|
+
{projectId: 'project-2', dataset: 'staging'},
|
|
223
|
+
{projectId: 'project-3', dataset: 'development'},
|
|
224
|
+
])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('preserves order of namedSources in config array', () => {
|
|
228
|
+
const namedSources = {
|
|
229
|
+
z: {projectId: 'project-z', dataset: 'z-dataset'},
|
|
230
|
+
a: {projectId: 'project-a', dataset: 'a-dataset'},
|
|
231
|
+
m: {projectId: 'project-m', dataset: 'm-dataset'},
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
235
|
+
|
|
236
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
237
|
+
const sanityAppElement = renderCall
|
|
238
|
+
|
|
239
|
+
// Object.values preserves insertion order in modern JS
|
|
240
|
+
expect(sanityAppElement.props.config).toEqual([
|
|
241
|
+
{projectId: 'project-z', dataset: 'z-dataset'},
|
|
242
|
+
{projectId: 'project-a', dataset: 'a-dataset'},
|
|
243
|
+
{projectId: 'project-m', dataset: 'm-dataset'},
|
|
244
|
+
])
|
|
181
245
|
})
|
|
182
246
|
|
|
183
247
|
it('passes children to SanityApp', () => {
|
|
248
|
+
const namedSources = {
|
|
249
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
250
|
+
}
|
|
184
251
|
const children = (
|
|
185
252
|
<div>
|
|
186
253
|
<h1>Test App</h1>
|
|
@@ -188,7 +255,7 @@ describe('renderSanityApp', () => {
|
|
|
188
255
|
</div>
|
|
189
256
|
)
|
|
190
257
|
|
|
191
|
-
renderSanityApp(rootElement,
|
|
258
|
+
renderSanityApp(rootElement, namedSources, {}, children)
|
|
192
259
|
|
|
193
260
|
const renderCall = mockRender.mock.calls[0][0]
|
|
194
261
|
const sanityAppElement = renderCall
|
|
@@ -197,8 +264,12 @@ describe('renderSanityApp', () => {
|
|
|
197
264
|
})
|
|
198
265
|
|
|
199
266
|
it('works with different types of children', () => {
|
|
267
|
+
const namedSources = {
|
|
268
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
269
|
+
}
|
|
270
|
+
|
|
200
271
|
// Test with string children
|
|
201
|
-
renderSanityApp(rootElement,
|
|
272
|
+
renderSanityApp(rootElement, namedSources, {}, 'String child')
|
|
202
273
|
|
|
203
274
|
let renderCall = mockRender.mock.calls[0][0]
|
|
204
275
|
let sanityAppElement = renderCall
|
|
@@ -207,7 +278,7 @@ describe('renderSanityApp', () => {
|
|
|
207
278
|
|
|
208
279
|
// Test with null children
|
|
209
280
|
mockRender.mockClear()
|
|
210
|
-
renderSanityApp(rootElement,
|
|
281
|
+
renderSanityApp(rootElement, namedSources, {}, null)
|
|
211
282
|
|
|
212
283
|
renderCall = mockRender.mock.calls[0][0]
|
|
213
284
|
sanityAppElement = renderCall
|
|
@@ -217,7 +288,7 @@ describe('renderSanityApp', () => {
|
|
|
217
288
|
// Test with array of children
|
|
218
289
|
mockRender.mockClear()
|
|
219
290
|
const arrayChildren = [<div key="1">Child 1</div>, <div key="2">Child 2</div>]
|
|
220
|
-
renderSanityApp(rootElement,
|
|
291
|
+
renderSanityApp(rootElement, namedSources, {}, arrayChildren)
|
|
221
292
|
|
|
222
293
|
renderCall = mockRender.mock.calls[0][0]
|
|
223
294
|
sanityAppElement = renderCall
|
|
@@ -225,19 +296,50 @@ describe('renderSanityApp', () => {
|
|
|
225
296
|
expect(sanityAppElement.props.children).toEqual(arrayChildren)
|
|
226
297
|
})
|
|
227
298
|
|
|
299
|
+
it('integrates with StrictMode and passes all props correctly', () => {
|
|
300
|
+
const namedSources = {
|
|
301
|
+
main: {
|
|
302
|
+
projectId: 'test-project',
|
|
303
|
+
dataset: 'production',
|
|
304
|
+
apiVersion: '2023-01-01',
|
|
305
|
+
} as SanityConfig,
|
|
306
|
+
secondary: {
|
|
307
|
+
projectId: 'test-project-2',
|
|
308
|
+
dataset: 'staging',
|
|
309
|
+
} as SanityConfig,
|
|
310
|
+
}
|
|
311
|
+
const children = <div>App Content</div>
|
|
312
|
+
|
|
313
|
+
renderSanityApp(rootElement, namedSources, {reactStrictMode: true}, children)
|
|
314
|
+
|
|
315
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
316
|
+
|
|
317
|
+
// Verify StrictMode wrapper (StrictMode is a Symbol in React 18)
|
|
318
|
+
expect(renderCall.type).toBeDefined()
|
|
319
|
+
expect(renderCall.type.toString()).toContain('Symbol')
|
|
320
|
+
|
|
321
|
+
// Verify SanityApp is inside StrictMode
|
|
322
|
+
const strictModeChild = renderCall.props.children
|
|
323
|
+
expect(strictModeChild.type).toBe(SanityApp)
|
|
324
|
+
|
|
325
|
+
// Verify all props are passed correctly
|
|
326
|
+
expect(strictModeChild.props.config).toEqual([
|
|
327
|
+
{projectId: 'test-project', dataset: 'production', apiVersion: '2023-01-01'},
|
|
328
|
+
{projectId: 'test-project-2', dataset: 'staging'},
|
|
329
|
+
])
|
|
330
|
+
expect(strictModeChild.props.fallback).toEqual(<div>Loading...</div>)
|
|
331
|
+
expect(strictModeChild.props.children).toEqual(children)
|
|
332
|
+
})
|
|
333
|
+
|
|
228
334
|
it('can be called multiple times with different roots', () => {
|
|
229
335
|
const rootElement2 = document.createElement('div')
|
|
230
336
|
document.body.appendChild(rootElement2)
|
|
231
337
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
const namedResources2 = {
|
|
236
|
-
default: {projectId: 'project-2', dataset: 'staging'},
|
|
237
|
-
}
|
|
338
|
+
const namedSources1 = {main: {projectId: 'project-1', dataset: 'production'}}
|
|
339
|
+
const namedSources2 = {main: {projectId: 'project-2', dataset: 'staging'}}
|
|
238
340
|
|
|
239
|
-
const unmount1 = renderSanityApp(rootElement,
|
|
240
|
-
const unmount2 = renderSanityApp(rootElement2,
|
|
341
|
+
const unmount1 = renderSanityApp(rootElement, namedSources1, {}, <div>App 1</div>)
|
|
342
|
+
const unmount2 = renderSanityApp(rootElement2, namedSources2, {}, <div>App 2</div>)
|
|
241
343
|
|
|
242
344
|
expect(mockCreateRoot).toHaveBeenCalledTimes(2)
|
|
243
345
|
expect(mockCreateRoot).toHaveBeenNthCalledWith(1, rootElement)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type SanityConfig} from '@sanity/sdk'
|
|
2
2
|
import {StrictMode} from 'react'
|
|
3
3
|
import {createRoot} from 'react-dom/client'
|
|
4
4
|
|
|
@@ -8,14 +8,16 @@ interface RenderSanitySDKAppOptions {
|
|
|
8
8
|
reactStrictMode?: boolean
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** In-flight CLI PR is using named sources since it's aspirational.
|
|
12
|
+
* We can transform the shape in this function until it's finalized.
|
|
13
|
+
*/
|
|
14
|
+
interface NamedSources {
|
|
15
|
+
[key: string]: SanityConfig
|
|
13
16
|
}
|
|
14
|
-
|
|
15
17
|
/** @internal */
|
|
16
18
|
export function renderSanityApp(
|
|
17
19
|
rootElement: HTMLElement | null,
|
|
18
|
-
|
|
20
|
+
namedSources: NamedSources,
|
|
19
21
|
options: RenderSanitySDKAppOptions,
|
|
20
22
|
children: React.ReactNode,
|
|
21
23
|
): () => void {
|
|
@@ -25,16 +27,18 @@ export function renderSanityApp(
|
|
|
25
27
|
const {reactStrictMode = false} = options
|
|
26
28
|
|
|
27
29
|
const root = createRoot(rootElement)
|
|
30
|
+
const config = Object.values(namedSources)
|
|
31
|
+
|
|
28
32
|
root.render(
|
|
29
33
|
reactStrictMode ? (
|
|
30
34
|
<StrictMode>
|
|
31
|
-
{/* TODO:
|
|
32
|
-
<SanityApp
|
|
35
|
+
{/* TODO: think about a loading component we want to be "universal" */}
|
|
36
|
+
<SanityApp config={config} fallback={<div>Loading...</div>}>
|
|
33
37
|
{children}
|
|
34
38
|
</SanityApp>
|
|
35
39
|
</StrictMode>
|
|
36
40
|
) : (
|
|
37
|
-
<SanityApp
|
|
41
|
+
<SanityApp config={config} fallback={<div>Loading...</div>}>
|
|
38
42
|
{children}
|
|
39
43
|
</SanityApp>
|
|
40
44
|
),
|