@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -155
- package/dist/index.d.ts +2488 -2019
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4435 -1529
- package/dist/index.js.map +1 -1
- package/package.json +55 -59
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +30 -18
- package/src/components/SDKProvider.test.tsx +173 -32
- package/src/components/SDKProvider.tsx +87 -28
- package/src/components/SanityApp.test.tsx +68 -12
- package/src/components/SanityApp.tsx +93 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +7 -7
- package/src/components/auth/AuthBoundary.tsx +29 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +104 -61
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/CorsErrorComponent.tsx +2 -2
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +29 -27
- package/src/constants.ts +5 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
- package/src/context/ComlinkTokenRefresh.tsx +3 -5
- package/src/context/DefaultResourceContext.ts +3 -3
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/PerspectiveContext.ts +3 -3
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +93 -90
- package/src/context/ResourceProvider.tsx +62 -93
- package/src/context/SDKStudioContext.test.tsx +28 -33
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/renderSanityApp.test.tsx +151 -49
- package/src/context/renderSanityApp.tsx +12 -8
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/agent/agentActions.test.tsx +2 -2
- package/src/hooks/agent/agentActions.ts +52 -72
- package/src/hooks/agent/useAgentResourceContext.ts +1 -1
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/client/useClient.test.tsx +1 -4
- package/src/hooks/client/useClient.ts +1 -0
- package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
- package/src/hooks/comlink/useFrameConnection.ts +1 -1
- package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
- package/src/hooks/comlink/useWindowConnection.ts +2 -3
- package/src/hooks/comments/useCommentActions.test.tsx +150 -0
- package/src/hooks/comments/useCommentActions.ts +109 -0
- package/src/hooks/comments/useCommentList.ts +79 -0
- package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
- package/src/hooks/comments/useCommentThreads.ts +73 -0
- package/src/hooks/comments/useComments.test.tsx +242 -0
- package/src/hooks/comments/useComments.ts +59 -0
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +18 -1
- package/src/hooks/context/useSanityInstance.ts +6 -6
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +11 -17
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -2
- package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
- package/src/hooks/dashboard/useWindowTitle.ts +114 -0
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +33 -13
- package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
- package/src/hooks/document/useApplyDocumentActions.ts +66 -48
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.test.tsx +37 -8
- package/src/hooks/document/useDocument.ts +131 -66
- package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
- package/src/hooks/document/useDocumentEvent.ts +16 -10
- package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
- package/src/hooks/document/useDocumentPermissions.ts +71 -39
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
- package/src/hooks/document/useEditDocument.test.tsx +21 -14
- package/src/hooks/document/useEditDocument.ts +163 -17
- package/src/hooks/documents/useDocuments.test.tsx +64 -44
- package/src/hooks/documents/useDocuments.ts +31 -20
- package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
- package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +226 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
- package/src/hooks/organizations/useOrganization.test.ts +63 -0
- package/src/hooks/organizations/useOrganization.ts +34 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
- package/src/hooks/organizations/useOrganizations.test.ts +63 -0
- package/src/hooks/organizations/useOrganizations.ts +39 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +35 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +22 -20
- package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
- package/src/hooks/presence/usePresenceForDocument.ts +104 -0
- package/src/hooks/presence/useReportPresence.test.tsx +202 -0
- package/src/hooks/presence/useReportPresence.ts +170 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +12 -14
- package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
- package/src/hooks/projection/useDocumentProjection.ts +81 -2
- package/src/hooks/projects/useProject.test-d.ts +56 -0
- package/src/hooks/projects/useProject.test.tsx +120 -0
- package/src/hooks/projects/useProject.ts +42 -43
- package/src/hooks/projects/useProjects.test-d.ts +60 -0
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +17 -32
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +94 -17
- package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
- package/src/hooks/releases/useActiveReleases.ts +28 -24
- package/src/hooks/releases/useAllReleases.test.tsx +93 -0
- package/src/hooks/releases/useAllReleases.ts +62 -0
- package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
- package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
- package/src/hooks/releases/usePerspective.test.tsx +18 -10
- package/src/hooks/releases/usePerspective.ts +13 -17
- package/src/hooks/users/useUser.test.tsx +3 -9
- package/src/hooks/users/useUser.ts +4 -8
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +38 -5
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/context/useDefaultResource.test.tsx +0 -25
- package/src/hooks/context/useDefaultResource.ts +0 -30
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
- package/src/hooks/datasets/useDatasets.test.ts +0 -66
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
- package/src/hooks/projects/useProject.test.ts +0 -79
|
@@ -1,65 +1,124 @@
|
|
|
1
1
|
import {
|
|
2
|
-
DEFAULT_RESOURCE_NAME,
|
|
3
2
|
type DocumentResource,
|
|
4
3
|
isDatasetResource,
|
|
4
|
+
isImportError,
|
|
5
5
|
type SanityConfig,
|
|
6
6
|
} from '@sanity/sdk'
|
|
7
|
-
import {type ReactElement, type ReactNode, useMemo} from 'react'
|
|
7
|
+
import {type ReactElement, type ReactNode, useEffect, useMemo} from 'react'
|
|
8
|
+
import {ErrorBoundary, type FallbackProps} from 'react-error-boundary'
|
|
8
9
|
|
|
10
|
+
import {DEFAULT_RESOURCE_NAME} from '../constants'
|
|
11
|
+
import {OrganizationResourcesProvider} from '../context/OrganizationResourcesProvider'
|
|
9
12
|
import {ResourceProvider} from '../context/ResourceProvider'
|
|
10
|
-
import {ResourcesContext} from '../context/ResourcesContext'
|
|
11
13
|
import {AuthBoundary, type AuthBoundaryProps} from './auth/AuthBoundary'
|
|
14
|
+
import {ChunkLoadError} from './errors/ChunkLoadError'
|
|
15
|
+
import {clearChunkReloadFlag} from './errors/chunkReloadStorage'
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* @internal
|
|
15
19
|
*/
|
|
16
20
|
export interface SDKProviderProps extends AuthBoundaryProps {
|
|
17
21
|
children: ReactNode
|
|
18
|
-
config: SanityConfig
|
|
19
|
-
/**
|
|
20
|
-
* Named document resources map. Provided to `ResourcesContext` for
|
|
21
|
-
* name-based resource resolution in hooks.
|
|
22
|
-
*/
|
|
23
|
-
resources?: Record<string, DocumentResource>
|
|
22
|
+
config: SanityConfig | SanityConfig[]
|
|
24
23
|
fallback: ReactNode
|
|
24
|
+
resources?: Record<string, DocumentResource>
|
|
25
|
+
/** When set, automatically fetches and registers the organization's media library and canvas as named resources. */
|
|
26
|
+
inferMediaLibraryAndCanvas?: boolean
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
30
|
+
* Clears the chunk-reload flag once children render successfully past the
|
|
31
|
+
* top-level boundary, so a future incident in the same session can trigger
|
|
32
|
+
* another automatic reload.
|
|
29
33
|
*/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return [...ids]
|
|
34
|
+
function ResetChunkReloadFlagOnMount(): null {
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
clearChunkReloadFlag()
|
|
37
|
+
}, [])
|
|
38
|
+
return null
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
39
42
|
* @internal
|
|
40
43
|
*
|
|
41
44
|
* Top-level context provider that provides access to the Sanity SDK.
|
|
42
|
-
*
|
|
43
|
-
* Creates a single `ResourceProvider` (and therefore a single `SanityInstance`)
|
|
44
|
-
* for the given config. Resource resolution is handled by `ResourcesContext`
|
|
45
|
-
* and the `"default"` named resource.
|
|
46
45
|
*/
|
|
47
46
|
export function SDKProvider({
|
|
48
47
|
children,
|
|
49
48
|
config,
|
|
50
|
-
resources = {},
|
|
51
49
|
fallback,
|
|
50
|
+
inferMediaLibraryAndCanvas,
|
|
52
51
|
...props
|
|
53
52
|
}: SDKProviderProps): ReactElement {
|
|
54
|
-
|
|
53
|
+
// For legacy config support, extract the first config object as a default resource,
|
|
54
|
+
// matching the legacy behavior nesting ResourceProviders with the first config closest to users' application code.
|
|
55
|
+
// This should be removed when we remove legacy config support.
|
|
56
|
+
const defaultConfig = Array.isArray(config) ? config[0] : config
|
|
57
|
+
const defaultProjectId = defaultConfig?.projectId
|
|
58
|
+
const defaultDataset = defaultConfig?.dataset
|
|
55
59
|
|
|
56
|
-
const
|
|
60
|
+
const resourcesValue = useMemo(() => {
|
|
61
|
+
const explicit = props.resources ?? {}
|
|
62
|
+
if (defaultProjectId && defaultDataset && !Object.hasOwn(explicit, DEFAULT_RESOURCE_NAME)) {
|
|
63
|
+
return {
|
|
64
|
+
[DEFAULT_RESOURCE_NAME]: {
|
|
65
|
+
projectId: defaultProjectId,
|
|
66
|
+
dataset: defaultDataset,
|
|
67
|
+
},
|
|
68
|
+
...explicit,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return explicit
|
|
72
|
+
}, [defaultProjectId, defaultDataset, props.resources])
|
|
73
|
+
|
|
74
|
+
// Collect all projectIds from both resources and config, deduped so
|
|
75
|
+
// AuthBoundary doesn't verify the same project more than once.
|
|
76
|
+
const projectIds = useMemo(
|
|
77
|
+
() =>
|
|
78
|
+
Array.from(
|
|
79
|
+
new Set(
|
|
80
|
+
[
|
|
81
|
+
...Object.values(resourcesValue)
|
|
82
|
+
.filter(isDatasetResource)
|
|
83
|
+
.map((resource) => resource.projectId),
|
|
84
|
+
...(Array.isArray(config) ? config : [config]).map((configObj) => configObj.projectId),
|
|
85
|
+
].filter((id): id is string => !!id),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
[resourcesValue, config],
|
|
89
|
+
)
|
|
57
90
|
|
|
58
91
|
return (
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
<ErrorBoundary FallbackComponent={ChunkAwareFallback}>
|
|
93
|
+
<ResetChunkReloadFlagOnMount />
|
|
94
|
+
{/* Spread the first config so SanityInstance can use the auth,
|
|
95
|
+
perspective, and other config fields; `resource` lets an explicitly
|
|
96
|
+
provided `default` resource override the config-derived resource. */}
|
|
97
|
+
<ResourceProvider
|
|
98
|
+
{...defaultConfig}
|
|
99
|
+
resource={resourcesValue[DEFAULT_RESOURCE_NAME]}
|
|
100
|
+
fallback={fallback}
|
|
101
|
+
>
|
|
102
|
+
<AuthBoundary {...props} projectIds={projectIds}>
|
|
103
|
+
{/* OrganizationResourcesProvider wraps ResourcesContext.
|
|
104
|
+
It merges explicit resources with lazily inferred org resources (media
|
|
105
|
+
library, canvas) and provides the combined map to the subtree. */}
|
|
106
|
+
<OrganizationResourcesProvider
|
|
107
|
+
resources={resourcesValue}
|
|
108
|
+
inferMediaLibraryAndCanvas={inferMediaLibraryAndCanvas}
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
</OrganizationResourcesProvider>
|
|
112
|
+
</AuthBoundary>
|
|
113
|
+
</ResourceProvider>
|
|
114
|
+
</ErrorBoundary>
|
|
64
115
|
)
|
|
65
116
|
}
|
|
117
|
+
|
|
118
|
+
function ChunkAwareFallback(fallbackProps: FallbackProps): ReactElement {
|
|
119
|
+
if (isImportError(fallbackProps.error)) {
|
|
120
|
+
return <ChunkLoadError {...fallbackProps} />
|
|
121
|
+
}
|
|
122
|
+
// Re-throw so downstream boundaries (e.g. AuthBoundary) handle other errors.
|
|
123
|
+
throw fallbackProps.error
|
|
124
|
+
}
|
|
@@ -58,11 +58,14 @@ describe('SanityApp', () => {
|
|
|
58
58
|
mockSDKProviderComponent.mockClear()
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
-
it('renders SDKProvider with a single
|
|
62
|
-
const
|
|
61
|
+
it('renders SDKProvider with a single config', () => {
|
|
62
|
+
const singleConfig = {
|
|
63
|
+
projectId: 'test-project',
|
|
64
|
+
dataset: 'production',
|
|
65
|
+
}
|
|
63
66
|
|
|
64
67
|
render(
|
|
65
|
-
<SanityApp
|
|
68
|
+
<SanityApp config={singleConfig} fallback={<div>Loading...</div>}>
|
|
66
69
|
<div>Child Content</div>
|
|
67
70
|
</SanityApp>,
|
|
68
71
|
)
|
|
@@ -77,17 +80,60 @@ describe('SanityApp', () => {
|
|
|
77
80
|
expect(firstCallArgs1).toBeDefined()
|
|
78
81
|
expect(firstCallArgs1.length).toBeGreaterThan(0)
|
|
79
82
|
const props = firstCallArgs1[0] as unknown as SDKProviderProps
|
|
83
|
+
const config = props?.config
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
// Config is now passed directly as an object for single configs
|
|
86
|
+
expect(config).toEqual(singleConfig)
|
|
82
87
|
expect(props.fallback).toBeTruthy()
|
|
83
88
|
})
|
|
84
89
|
|
|
90
|
+
it('renders SDKProvider with multiple configs in original order', () => {
|
|
91
|
+
const multipleConfigs = [
|
|
92
|
+
{
|
|
93
|
+
projectId: 'project-1',
|
|
94
|
+
dataset: 'production',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
projectId: 'project-2',
|
|
98
|
+
dataset: 'staging',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
projectId: 'project-3',
|
|
102
|
+
dataset: 'development',
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
render(
|
|
107
|
+
<SanityApp config={multipleConfigs} fallback={<div>Loading...</div>}>
|
|
108
|
+
<div>Child Content</div>
|
|
109
|
+
</SanityApp>,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// Check that the SDKProvider is rendered
|
|
113
|
+
expect(screen.getByTestId('sdk-provider')).toBeInTheDocument()
|
|
114
|
+
|
|
115
|
+
// Verify SDKProvider was called with the correct props
|
|
116
|
+
expect(mockSDKProviderComponent).toHaveBeenCalledTimes(1)
|
|
117
|
+
const sdkProviderCalls = mockSDKProviderComponent.mock.calls
|
|
118
|
+
const firstCallArgs2 = sdkProviderCalls[0]
|
|
119
|
+
expect(firstCallArgs2).toBeDefined()
|
|
120
|
+
expect(firstCallArgs2.length).toBeGreaterThan(0)
|
|
121
|
+
const props = firstCallArgs2[0] as unknown as SDKProviderProps
|
|
122
|
+
const config = props?.config
|
|
123
|
+
|
|
124
|
+
// Config should be passed directly to SDKProvider
|
|
125
|
+
expect(config).toEqual(multipleConfigs)
|
|
126
|
+
})
|
|
127
|
+
|
|
85
128
|
it('handles iframe environment correctly', async () => {
|
|
86
129
|
// Mock window.self and window.top to simulate iframe environment
|
|
87
130
|
const originalTop = window.top
|
|
88
131
|
const originalSelf = window.self
|
|
89
132
|
|
|
90
|
-
const mockSanityConfig: SanityConfig = {
|
|
133
|
+
const mockSanityConfig: SanityConfig = {
|
|
134
|
+
projectId: 'test-project',
|
|
135
|
+
dataset: 'test-dataset',
|
|
136
|
+
}
|
|
91
137
|
|
|
92
138
|
const mockTop = {}
|
|
93
139
|
Object.defineProperty(window, 'top', {
|
|
@@ -100,7 +146,7 @@ describe('SanityApp', () => {
|
|
|
100
146
|
})
|
|
101
147
|
|
|
102
148
|
render(
|
|
103
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
149
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
104
150
|
<div>Test Child</div>
|
|
105
151
|
</SanityApp>,
|
|
106
152
|
)
|
|
@@ -131,7 +177,10 @@ describe('SanityApp', () => {
|
|
|
131
177
|
href: 'http://sanity-test.app',
|
|
132
178
|
}
|
|
133
179
|
|
|
134
|
-
const mockSanityConfig: SanityConfig = {
|
|
180
|
+
const mockSanityConfig: SanityConfig = {
|
|
181
|
+
projectId: 'test-project',
|
|
182
|
+
dataset: 'test-dataset',
|
|
183
|
+
}
|
|
135
184
|
|
|
136
185
|
Object.defineProperty(window, 'location', {
|
|
137
186
|
value: mockLocation,
|
|
@@ -139,7 +188,7 @@ describe('SanityApp', () => {
|
|
|
139
188
|
})
|
|
140
189
|
|
|
141
190
|
render(
|
|
142
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
191
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
143
192
|
<div>Test Child</div>
|
|
144
193
|
</SanityApp>,
|
|
145
194
|
)
|
|
@@ -192,7 +241,10 @@ describe('SanityApp', () => {
|
|
|
192
241
|
it('does not redirect to core if not inside iframe and local url', async () => {
|
|
193
242
|
const originalLocation = window.location
|
|
194
243
|
|
|
195
|
-
const mockSanityConfig: SanityConfig = {
|
|
244
|
+
const mockSanityConfig: SanityConfig = {
|
|
245
|
+
projectId: 'test-project',
|
|
246
|
+
dataset: 'test-dataset',
|
|
247
|
+
}
|
|
196
248
|
|
|
197
249
|
const mockLocation = {
|
|
198
250
|
replace: vi.fn(),
|
|
@@ -205,7 +257,7 @@ describe('SanityApp', () => {
|
|
|
205
257
|
})
|
|
206
258
|
|
|
207
259
|
render(
|
|
208
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
260
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
209
261
|
<div>Test Child</div>
|
|
210
262
|
</SanityApp>,
|
|
211
263
|
)
|
|
@@ -233,6 +285,8 @@ describe('SanityApp', () => {
|
|
|
233
285
|
}
|
|
234
286
|
|
|
235
287
|
const mockSanityConfig: SanityConfig = {
|
|
288
|
+
projectId: 'test-project',
|
|
289
|
+
dataset: 'test-dataset',
|
|
236
290
|
studio: {},
|
|
237
291
|
}
|
|
238
292
|
|
|
@@ -242,7 +296,7 @@ describe('SanityApp', () => {
|
|
|
242
296
|
})
|
|
243
297
|
|
|
244
298
|
render(
|
|
245
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
299
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
246
300
|
<div>Test Child</div>
|
|
247
301
|
</SanityApp>,
|
|
248
302
|
)
|
|
@@ -271,6 +325,8 @@ describe('SanityApp', () => {
|
|
|
271
325
|
}
|
|
272
326
|
|
|
273
327
|
const mockSanityConfig: SanityConfig = {
|
|
328
|
+
projectId: 'test-project',
|
|
329
|
+
dataset: 'test-dataset',
|
|
274
330
|
studio: {},
|
|
275
331
|
}
|
|
276
332
|
|
|
@@ -280,7 +336,7 @@ describe('SanityApp', () => {
|
|
|
280
336
|
})
|
|
281
337
|
|
|
282
338
|
render(
|
|
283
|
-
<SanityApp config={mockSanityConfig} fallback={<div>Fallback</div>}>
|
|
339
|
+
<SanityApp config={[mockSanityConfig]} fallback={<div>Fallback</div>}>
|
|
284
340
|
<div>Test Child</div>
|
|
285
341
|
</SanityApp>,
|
|
286
342
|
)
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type DocumentResource,
|
|
4
|
-
isStudioConfig,
|
|
5
|
-
type SanityConfig,
|
|
6
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {type DocumentResource, type SanityConfig} from '@sanity/sdk'
|
|
2
|
+
import {isStudioConfig} from '@sanity/sdk/_internal'
|
|
7
3
|
import {type ReactElement, useContext, useEffect, useMemo} from 'react'
|
|
8
4
|
|
|
9
5
|
import {SDKStudioContext, type StudioWorkspaceHandle} from '../context/SDKStudioContext'
|
|
@@ -16,41 +12,61 @@ import {isInIframe, isLocalUrl} from './utils'
|
|
|
16
12
|
*/
|
|
17
13
|
export interface SanityAppProps {
|
|
18
14
|
/**
|
|
19
|
-
*
|
|
20
|
-
* Optional when `SanityApp` is rendered inside an `SDKStudioContext`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*/
|
|
24
|
-
config?: SanityConfig
|
|
25
|
-
/**
|
|
26
|
-
* Named document resources for the application. The resource keyed `"default"`
|
|
27
|
-
* is used automatically when no explicit resource is specified in hooks.
|
|
15
|
+
* One or more SanityConfig objects providing a project ID and dataset name.
|
|
16
|
+
* Optional when `SanityApp` is rendered inside an `SDKStudioContext` provider
|
|
17
|
+
* (e.g. inside Sanity Studio) — the config is derived from the workspace
|
|
18
|
+
* automatically.
|
|
28
19
|
*/
|
|
20
|
+
config?: SanityConfig | SanityConfig[]
|
|
29
21
|
resources?: Record<string, DocumentResource>
|
|
30
22
|
children: React.ReactNode
|
|
31
23
|
/* Fallback content to show when child components are suspending. Same as the `fallback` prop for React Suspense. */
|
|
32
24
|
fallback: React.ReactNode
|
|
25
|
+
/**
|
|
26
|
+
* Set this to automatically fetch and register the organization's media library and canvas as named resources.
|
|
27
|
+
* These resources will be available to hooks as `media-library` and `canvas`.
|
|
28
|
+
*
|
|
29
|
+
* The SDK App must be running in the organization's Dashboard to use this feature.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
*
|
|
34
|
+
* const MyApp = () => {
|
|
35
|
+
* // should "just work" because of inferMediaLibraryAndCanvas.
|
|
36
|
+
* const {data: assets} = useDocuments({
|
|
37
|
+
* documentType: 'sanity.asset',
|
|
38
|
+
* resourceName: 'media-library',
|
|
39
|
+
* })
|
|
40
|
+
* return (
|
|
41
|
+
* <div>
|
|
42
|
+
* {assets.map((asset) => (
|
|
43
|
+
* <div key={asset._id}>{asset.originalFilename}</div>
|
|
44
|
+
* ))}
|
|
45
|
+
* </div>
|
|
46
|
+
* )
|
|
47
|
+
* }
|
|
48
|
+
* <SanityApp inferMediaLibraryAndCanvas>
|
|
49
|
+
* <MyApp />
|
|
50
|
+
* </SanityApp>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
inferMediaLibraryAndCanvas?: boolean
|
|
33
54
|
}
|
|
34
55
|
|
|
35
56
|
const REDIRECT_URL = 'https://sanity.io/welcome'
|
|
36
57
|
|
|
37
58
|
/**
|
|
38
|
-
* Derive a SanityConfig
|
|
59
|
+
* Derive a SanityConfig from a Studio workspace handle.
|
|
60
|
+
* Maps the workspace's projectId, dataset, and reactive auth token into
|
|
61
|
+
* the SDK's config shape.
|
|
39
62
|
*/
|
|
40
|
-
function
|
|
41
|
-
config: SanityConfig
|
|
42
|
-
resources: Record<string, DocumentResource>
|
|
43
|
-
} {
|
|
63
|
+
function deriveConfigFromWorkspace(workspace: StudioWorkspaceHandle): SanityConfig {
|
|
44
64
|
return {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
resources: {
|
|
53
|
-
[DEFAULT_RESOURCE_NAME]: {projectId: workspace.projectId, dataset: workspace.dataset},
|
|
65
|
+
projectId: workspace.projectId,
|
|
66
|
+
dataset: workspace.dataset,
|
|
67
|
+
studio: {
|
|
68
|
+
authenticated: workspace.authenticated,
|
|
69
|
+
auth: workspace.auth.token ? {token: workspace.auth.token} : undefined,
|
|
54
70
|
},
|
|
55
71
|
}
|
|
56
72
|
}
|
|
@@ -62,49 +78,57 @@ function deriveFromWorkspace(workspace: StudioWorkspaceHandle): {
|
|
|
62
78
|
* as well as application context and state which is used by the Sanity React hooks. Your application
|
|
63
79
|
* must be wrapped with the SanityApp component to function properly.
|
|
64
80
|
*
|
|
65
|
-
* The `config` prop accepts a {@link SanityConfig} object
|
|
66
|
-
* one or more
|
|
81
|
+
* The `config` prop on the SanityApp component accepts either a single {@link SanityConfig} object, or an array of them.
|
|
82
|
+
* This allows your app to work with one or more of your organization's datasets.
|
|
67
83
|
*
|
|
68
|
-
* When rendered inside a Sanity Studio that provides `SDKStudioContext`, the `config`
|
|
69
|
-
*
|
|
84
|
+
* When rendered inside a Sanity Studio that provides `SDKStudioContext`, the `config` prop is
|
|
85
|
+
* optional — `SanityApp` will automatically derive `projectId`, `dataset`, and auth from the
|
|
86
|
+
* Studio workspace.
|
|
70
87
|
*
|
|
71
|
-
*
|
|
88
|
+
* @remarks
|
|
89
|
+
* When passing multiple SanityConfig objects to the `config` prop, the first configuration in the array becomes the default
|
|
90
|
+
* configuration used by the App SDK Hooks.
|
|
91
|
+
*
|
|
92
|
+
* When both `config` and `SDKStudioContext` are available, the explicit `config` takes precedence.
|
|
72
93
|
*
|
|
73
94
|
* @category Components
|
|
74
95
|
* @param props - Your Sanity configuration and the React children to render
|
|
75
96
|
* @returns Your Sanity application, integrated with your Sanity configuration and application context
|
|
76
97
|
*
|
|
77
|
-
* @example
|
|
98
|
+
* @example
|
|
78
99
|
* ```tsx
|
|
79
|
-
* import { SanityApp } from '@sanity/sdk-react'
|
|
100
|
+
* import { SanityApp, type SanityConfig } from '@sanity/sdk-react'
|
|
80
101
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* fallback={<div>Loading…</div>}
|
|
88
|
-
* >
|
|
89
|
-
* <MyAppRoot />
|
|
90
|
-
* </SanityApp>
|
|
91
|
-
* )
|
|
102
|
+
* import MyAppRoot from './Root'
|
|
103
|
+
*
|
|
104
|
+
* // Single project configuration
|
|
105
|
+
* const mySanityConfig: SanityConfig = {
|
|
106
|
+
* projectId: 'my-project-id',
|
|
107
|
+
* dataset: 'production',
|
|
92
108
|
* }
|
|
93
|
-
* ```
|
|
94
109
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
110
|
+
* // Or multiple project configurations
|
|
111
|
+
* const multipleConfigs: SanityConfig[] = [
|
|
112
|
+
* // Configuration for your main project. This will be used as the default project for hooks.
|
|
113
|
+
* {
|
|
114
|
+
* projectId: 'marketing-website-project',
|
|
115
|
+
* dataset: 'production',
|
|
116
|
+
* },
|
|
117
|
+
* // Configuration for a separate blog project
|
|
118
|
+
* {
|
|
119
|
+
* projectId: 'blog-project',
|
|
120
|
+
* dataset: 'production',
|
|
121
|
+
* },
|
|
122
|
+
* // Configuration for a separate ecommerce project
|
|
123
|
+
* {
|
|
124
|
+
* projectId: 'ecommerce-project',
|
|
125
|
+
* dataset: 'production',
|
|
126
|
+
* }
|
|
127
|
+
* ]
|
|
98
128
|
*
|
|
99
129
|
* export default function MyApp() {
|
|
100
130
|
* return (
|
|
101
|
-
* <SanityApp
|
|
102
|
-
* resources={{
|
|
103
|
-
* default: { projectId: 'abc123', dataset: 'production' },
|
|
104
|
-
* 'blog-project': { projectId: 'def456', dataset: 'production' },
|
|
105
|
-
* }}
|
|
106
|
-
* fallback={<div>Loading…</div>}
|
|
107
|
-
* >
|
|
131
|
+
* <SanityApp config={mySanityConfig} fallback={<div>Loading…</div>}>
|
|
108
132
|
* <MyAppRoot />
|
|
109
133
|
* </SanityApp>
|
|
110
134
|
* )
|
|
@@ -115,42 +139,29 @@ export function SanityApp({
|
|
|
115
139
|
children,
|
|
116
140
|
fallback,
|
|
117
141
|
config: configProp,
|
|
118
|
-
resources: resourcesProp,
|
|
119
142
|
...props
|
|
120
143
|
}: SanityAppProps): ReactElement {
|
|
121
144
|
const studioWorkspace = useContext(SDKStudioContext)
|
|
122
145
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
}, [configProp,
|
|
129
|
-
|
|
130
|
-
const resolvedConfig = useMemo<SanityConfig>(() => {
|
|
131
|
-
if (configProp) {
|
|
132
|
-
return configProp
|
|
133
|
-
}
|
|
134
|
-
if (derived) return derived.config
|
|
135
|
-
return {}
|
|
136
|
-
}, [configProp, derived])
|
|
137
|
-
|
|
138
|
-
const resolvedResources = useMemo<Record<string, DocumentResource>>(() => {
|
|
139
|
-
if (resourcesProp) return resourcesProp
|
|
140
|
-
if (derived) return derived.resources
|
|
141
|
-
return {}
|
|
142
|
-
}, [resourcesProp, derived])
|
|
146
|
+
// Derive config: explicit config takes precedence, then Studio context
|
|
147
|
+
const resolvedConfig = useMemo(() => {
|
|
148
|
+
if (configProp) return configProp
|
|
149
|
+
if (studioWorkspace) return deriveConfigFromWorkspace(studioWorkspace)
|
|
150
|
+
return []
|
|
151
|
+
}, [configProp, studioWorkspace])
|
|
143
152
|
|
|
144
153
|
useEffect(() => {
|
|
145
154
|
let timeout: NodeJS.Timeout | undefined
|
|
155
|
+
const primaryConfig = Array.isArray(resolvedConfig) ? resolvedConfig[0] : resolvedConfig
|
|
146
156
|
const shouldRedirectWithoutConfig =
|
|
147
|
-
configProp === undefined && !studioWorkspace && !
|
|
157
|
+
configProp === undefined && !studioWorkspace && !primaryConfig
|
|
148
158
|
|
|
149
159
|
if (
|
|
150
160
|
!isInIframe() &&
|
|
151
161
|
!isLocalUrl(window) &&
|
|
152
|
-
(shouldRedirectWithoutConfig || (!!
|
|
162
|
+
(shouldRedirectWithoutConfig || (!!primaryConfig && !isStudioConfig(primaryConfig)))
|
|
153
163
|
) {
|
|
164
|
+
// If the app is not running in an iframe and is not a local url, redirect to core.
|
|
154
165
|
timeout = setTimeout(() => {
|
|
155
166
|
// eslint-disable-next-line no-console
|
|
156
167
|
console.warn('Redirecting to core', REDIRECT_URL)
|
|
@@ -161,12 +172,7 @@ export function SanityApp({
|
|
|
161
172
|
}, [configProp, resolvedConfig, studioWorkspace])
|
|
162
173
|
|
|
163
174
|
return (
|
|
164
|
-
<SDKProvider
|
|
165
|
-
{...props}
|
|
166
|
-
fallback={fallback}
|
|
167
|
-
config={resolvedConfig}
|
|
168
|
-
resources={resolvedResources}
|
|
169
|
-
>
|
|
175
|
+
<SDKProvider {...props} fallback={fallback} config={resolvedConfig}>
|
|
170
176
|
{children}
|
|
171
177
|
</SDKProvider>
|
|
172
178
|
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {AuthStateType, getIsInDashboardState} from '@sanity/sdk'
|
|
2
|
+
import {render, screen, waitFor} from '@testing-library/react'
|
|
3
|
+
import {beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
6
|
+
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
7
|
+
import {AuthBoundary} from './AuthBoundary'
|
|
8
|
+
|
|
9
|
+
// NOTE: unlike AuthBoundary.test.tsx this file does NOT mock react-error-boundary
|
|
10
|
+
// — we want the REAL ErrorBoundary so we can observe whether it recovers when the
|
|
11
|
+
// auth store transitions back to LOGGED_IN (e.g. after a successful silent token
|
|
12
|
+
// refresh via ComlinkTokenRefresh -> setAuthToken).
|
|
13
|
+
|
|
14
|
+
vi.mock('@sanity/sdk', async () => {
|
|
15
|
+
const actual = await vi.importActual('@sanity/sdk')
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
getIsInDashboardState: vi.fn(() => ({getCurrent: () => true})),
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
vi.mock('../../hooks/auth/useAuthState', () => ({useAuthState: vi.fn()}))
|
|
23
|
+
vi.mock('../../hooks/auth/useLoginUrl', () => ({
|
|
24
|
+
useLoginUrl: vi.fn(() => 'https://example.com/login'),
|
|
25
|
+
}))
|
|
26
|
+
vi.mock('../../hooks/auth/useVerifyOrgProjects', () => ({useVerifyOrgProjects: vi.fn(() => null)}))
|
|
27
|
+
vi.mock('../../hooks/auth/useLogOut', () => ({useLogOut: vi.fn(() => async () => {})}))
|
|
28
|
+
vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
29
|
+
useHandleAuthCallback: vi.fn(() => async () => {}),
|
|
30
|
+
}))
|
|
31
|
+
vi.mock('../../hooks/comlink/useWindowConnection', () => ({
|
|
32
|
+
useWindowConnection: vi.fn(() => ({fetch: vi.fn().mockResolvedValue({token: 'fresh-token'})})),
|
|
33
|
+
}))
|
|
34
|
+
// Avoid injecting the SanityOS bridge.js <script> during import.
|
|
35
|
+
vi.mock('../utils', () => ({isInIframe: vi.fn(() => false)}))
|
|
36
|
+
|
|
37
|
+
const mockUseAuthState = useAuthState as Mock
|
|
38
|
+
|
|
39
|
+
describe('AuthBoundary — recovery after silent token refresh (dashboard)', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks()
|
|
42
|
+
;(getIsInDashboardState as Mock).mockReturnValue({getCurrent: () => true})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('renders the app again once the session is re-established (LOGGED_IN)', async () => {
|
|
46
|
+
// 1. Session expires: a request 401s and the auth store goes to ERROR.
|
|
47
|
+
mockUseAuthState.mockReturnValue({
|
|
48
|
+
type: AuthStateType.ERROR,
|
|
49
|
+
error: Object.assign(new Error('Unauthorized'), {statusCode: 401}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const {rerender} = render(
|
|
53
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
54
|
+
<AuthBoundary projectIds={['p']}>
|
|
55
|
+
<div>Protected Content</div>
|
|
56
|
+
</AuthBoundary>
|
|
57
|
+
</ResourceProvider>,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// The error boundary catches the AuthError and shows the auth-error screen.
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByText('Authentication Error')).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// 2. ComlinkTokenRefresh fetches a fresh token from the Dashboard, setAuthToken
|
|
66
|
+
// lands it, /users/me re-fetches and the store returns to LOGGED_IN.
|
|
67
|
+
mockUseAuthState.mockReturnValue({
|
|
68
|
+
type: AuthStateType.LOGGED_IN,
|
|
69
|
+
currentUser: null,
|
|
70
|
+
token: 'fresh-token',
|
|
71
|
+
})
|
|
72
|
+
rerender(
|
|
73
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
74
|
+
<AuthBoundary projectIds={['p']}>
|
|
75
|
+
<div>Protected Content</div>
|
|
76
|
+
</AuthBoundary>
|
|
77
|
+
</ResourceProvider>,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// EXPECTED (desired) behaviour: the app recovers automatically, no Retry click.
|
|
81
|
+
await waitFor(() => {
|
|
82
|
+
expect(screen.getByText('Protected Content')).toBeInTheDocument()
|
|
83
|
+
})
|
|
84
|
+
expect(screen.queryByText('Authentication Error')).not.toBeInTheDocument()
|
|
85
|
+
})
|
|
86
|
+
})
|