@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,56 @@
|
|
|
1
|
+
import {useEffect} from 'react'
|
|
2
|
+
import {type FallbackProps} from 'react-error-boundary'
|
|
3
|
+
|
|
4
|
+
import {clearChunkReloadFlag, readChunkReloadFlag, setChunkReloadFlag} from './chunkReloadStorage'
|
|
5
|
+
import {Error} from './Error'
|
|
6
|
+
|
|
7
|
+
function reload(): void {
|
|
8
|
+
try {
|
|
9
|
+
window.location.reload()
|
|
10
|
+
} catch {
|
|
11
|
+
// No-op: nothing useful we can do if reload itself throws.
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default fallback rendered when a dynamic-import or chunk-load error
|
|
17
|
+
* bubbles up to the SDK's top-level error boundary.
|
|
18
|
+
*
|
|
19
|
+
* On the first occurrence in a session we set a flag and trigger
|
|
20
|
+
* window.location.reload(), since chunk-load errors almost always indicate a
|
|
21
|
+
* stale tab that simply needs a fresh index.html. If the flag is already set
|
|
22
|
+
* we render a manual reload UI instead, which prevents an infinite reload
|
|
23
|
+
* loop in the rare case the error is genuinely unrecoverable (network
|
|
24
|
+
* outage, CSP, etc.).
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export function ChunkLoadError(_props: FallbackProps): React.ReactNode {
|
|
29
|
+
const alreadyAttempted = readChunkReloadFlag()
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (alreadyAttempted) return
|
|
33
|
+
setChunkReloadFlag()
|
|
34
|
+
reload()
|
|
35
|
+
}, [alreadyAttempted])
|
|
36
|
+
|
|
37
|
+
if (!alreadyAttempted) {
|
|
38
|
+
// Render nothing during the brief window before the page reloads so the
|
|
39
|
+
// user does not see a flash of error UI.
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Error
|
|
45
|
+
heading="A new version is available"
|
|
46
|
+
description="The page tried to load an asset that no longer exists. Reload to continue with the latest version."
|
|
47
|
+
cta={{
|
|
48
|
+
text: 'Reload page',
|
|
49
|
+
onClick: () => {
|
|
50
|
+
clearChunkReloadFlag()
|
|
51
|
+
reload()
|
|
52
|
+
},
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {useMemo} from 'react'
|
|
2
|
-
import {type FallbackProps} from 'react-error-boundary'
|
|
2
|
+
import {type FallbackProps, getErrorMessage} from 'react-error-boundary'
|
|
3
3
|
|
|
4
4
|
import {Error} from './Error'
|
|
5
5
|
|
|
@@ -30,7 +30,7 @@ export function CorsErrorComponent({projectId, error}: CorsErrorComponentProps):
|
|
|
30
30
|
},
|
|
31
31
|
}
|
|
32
32
|
: {
|
|
33
|
-
description: error
|
|
33
|
+
description: getErrorMessage(error),
|
|
34
34
|
})}
|
|
35
35
|
/>
|
|
36
36
|
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-storage key tracking whether the SDK has already attempted an
|
|
3
|
+
* automatic reload in response to a chunk-load error during this session.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export const CHUNK_RELOAD_STORAGE_KEY = '__sanity_sdk_chunk_reload_attempted'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true when this session has already triggered an automatic reload.
|
|
11
|
+
* Returns false if session storage is unreadable.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export function readChunkReloadFlag(): boolean {
|
|
16
|
+
try {
|
|
17
|
+
if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
return window.sessionStorage.getItem(CHUNK_RELOAD_STORAGE_KEY) !== null
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Marks the session as having attempted an automatic reload, so the next
|
|
28
|
+
* chunk-load error renders the manual reload UI instead of looping.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export function setChunkReloadFlag(): void {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') return
|
|
35
|
+
window.sessionStorage.setItem(CHUNK_RELOAD_STORAGE_KEY, '1')
|
|
36
|
+
} catch {
|
|
37
|
+
// Storage may be unavailable (private mode quotas, disabled cookies).
|
|
38
|
+
// Falling through means the user sees the manual-reload UI instead of an
|
|
39
|
+
// automatic reload, which is the correct degradation.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Clears the chunk-reload flag. Called from SDKProvider once the SDK
|
|
45
|
+
* mounts successfully past the error boundary so a future incident in the
|
|
46
|
+
* same session can trigger another automatic reload.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export function clearChunkReloadFlag(): void {
|
|
51
|
+
try {
|
|
52
|
+
if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') return
|
|
53
|
+
window.sessionStorage.removeItem(CHUNK_RELOAD_STORAGE_KEY)
|
|
54
|
+
} catch {
|
|
55
|
+
// No-op: see setChunkReloadFlag.
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/config/handles.ts
CHANGED
|
@@ -1,53 +1,55 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
type DatasetHandle,
|
|
3
|
+
type DocumentHandle as CoreDocumentHandle,
|
|
4
|
+
type DocumentTypeHandle as CoreDocumentTypeHandle,
|
|
5
|
+
} from '@sanity/sdk'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* SDK
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* React SDK resource handle — extends the core DatasetHandle with `resourceName`
|
|
9
|
+
* for context-based resource resolution.
|
|
10
|
+
*
|
|
11
|
+
* Use this (or its subtypes) as the options type for custom hooks that need to
|
|
12
|
+
* accept a resource. It accepts a `resource` object, a `resourceName` registered
|
|
13
|
+
* via the `resources` prop on `<SanityApp>`, or a bare `projectId`/`dataset` pair
|
|
14
|
+
* for backward compatibility.
|
|
15
|
+
*
|
|
12
16
|
* @public
|
|
13
17
|
*/
|
|
14
18
|
export interface ResourceHandle<
|
|
15
|
-
TProjectId extends string = string,
|
|
16
19
|
TDataset extends string = string,
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
TProjectId extends string = string,
|
|
21
|
+
> extends DatasetHandle<TDataset, TProjectId> {
|
|
22
|
+
/**
|
|
23
|
+
* Name of a resource registered via the `resources` prop on `<SanityApp>`.
|
|
24
|
+
* Resolved to a `DocumentResource` at the React layer.
|
|
25
|
+
*/
|
|
19
26
|
resourceName?: string
|
|
20
|
-
perspective?: PerspectiveHandle['perspective']
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
|
-
* SDK
|
|
25
|
-
* Resource is resolved from context when not provided.
|
|
26
|
-
* When a `resourceName` is provided, the resource will be resolved from the context using the `ResourcesContext`,
|
|
27
|
-
* if there is a matching resource by that name.
|
|
30
|
+
* React SDK document-type handle. Adds `resourceName` to the core `DocumentTypeHandle`.
|
|
28
31
|
* @public
|
|
29
32
|
*/
|
|
30
33
|
export interface DocumentTypeHandle<
|
|
31
34
|
TDocumentType extends string = string,
|
|
32
35
|
TDataset extends string = string,
|
|
33
36
|
TProjectId extends string = string,
|
|
34
|
-
> extends
|
|
35
|
-
|
|
36
|
-
documentId?: string
|
|
37
|
-
liveEdit?: boolean
|
|
37
|
+
> extends CoreDocumentTypeHandle<TDocumentType, TDataset, TProjectId> {
|
|
38
|
+
resourceName?: string
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
|
-
* SDK
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* React SDK document handle. Adds `resourceName` to the core `DocumentHandle`.
|
|
43
|
+
*
|
|
44
|
+
* Import from `@sanity/sdk-react` (not `@sanity/sdk`) when writing option types
|
|
45
|
+
* for hooks — this version understands `resourceName` resolution.
|
|
46
|
+
*
|
|
45
47
|
* @public
|
|
46
48
|
*/
|
|
47
49
|
export interface DocumentHandle<
|
|
48
50
|
TDocumentType extends string = string,
|
|
49
51
|
TDataset extends string = string,
|
|
50
52
|
TProjectId extends string = string,
|
|
51
|
-
> extends
|
|
52
|
-
|
|
53
|
+
> extends CoreDocumentHandle<TDocumentType, TDataset, TProjectId> {
|
|
54
|
+
resourceName?: string
|
|
53
55
|
}
|
package/src/constants.ts
ADDED
|
@@ -56,10 +56,7 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
56
56
|
it('should not request new token on 401 if not in dashboard', async () => {
|
|
57
57
|
mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
|
|
58
58
|
const {rerender} = render(
|
|
59
|
-
<ResourceProvider
|
|
60
|
-
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
61
|
-
fallback={null}
|
|
62
|
-
>
|
|
59
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
63
60
|
<ComlinkTokenRefreshProvider>
|
|
64
61
|
<div>Test</div>
|
|
65
62
|
</ComlinkTokenRefreshProvider>
|
|
@@ -72,10 +69,7 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
72
69
|
})
|
|
73
70
|
act(() => {
|
|
74
71
|
rerender(
|
|
75
|
-
<ResourceProvider
|
|
76
|
-
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
77
|
-
fallback={null}
|
|
78
|
-
>
|
|
72
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
79
73
|
<ComlinkTokenRefreshProvider>
|
|
80
74
|
<div>Test</div>
|
|
81
75
|
</ComlinkTokenRefreshProvider>
|
|
@@ -98,10 +92,7 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
98
92
|
it('should initialize useWindowConnection with correct parameters when not in studio mode', () => {
|
|
99
93
|
// Simulate studio mode disabled by default
|
|
100
94
|
render(
|
|
101
|
-
<ResourceProvider
|
|
102
|
-
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
103
|
-
fallback={null}
|
|
104
|
-
>
|
|
95
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
105
96
|
<ComlinkTokenRefreshProvider>
|
|
106
97
|
<div>Test</div>
|
|
107
98
|
</ComlinkTokenRefreshProvider>
|
|
@@ -133,10 +124,7 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
133
124
|
document.body.appendChild(errorContainer)
|
|
134
125
|
|
|
135
126
|
render(
|
|
136
|
-
<ResourceProvider
|
|
137
|
-
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
138
|
-
fallback={null}
|
|
139
|
-
>
|
|
127
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
140
128
|
<ComlinkTokenRefreshProvider>
|
|
141
129
|
<div>Test</div>
|
|
142
130
|
</ComlinkTokenRefreshProvider>
|
|
@@ -150,10 +138,11 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
150
138
|
expect(mockSetAuthToken).toHaveBeenCalledWith(expect.any(Object), 'new-token')
|
|
151
139
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
152
140
|
expect(mockFetch).toHaveBeenCalledWith('dashboard/v1/auth/tokens/create')
|
|
153
|
-
// Assert setAuthToken was called with
|
|
141
|
+
// Assert setAuthToken was called with instance matching provider config
|
|
154
142
|
const instanceArg = mockSetAuthToken.mock.calls[0][0]
|
|
155
|
-
expect(instanceArg).
|
|
156
|
-
|
|
143
|
+
expect(instanceArg.config).toEqual(
|
|
144
|
+
expect.objectContaining({projectId: 'test-project', dataset: 'test-dataset'}),
|
|
145
|
+
)
|
|
157
146
|
// Unauthorized error container should be removed
|
|
158
147
|
expect(document.getElementById('__sanityError')).toBeNull()
|
|
159
148
|
})
|
|
@@ -166,10 +155,7 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
166
155
|
mockFetch.mockResolvedValueOnce({token: null})
|
|
167
156
|
|
|
168
157
|
render(
|
|
169
|
-
<ResourceProvider
|
|
170
|
-
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
171
|
-
fallback={null}
|
|
172
|
-
>
|
|
158
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
173
159
|
<ComlinkTokenRefreshProvider>
|
|
174
160
|
<div>Test</div>
|
|
175
161
|
</ComlinkTokenRefreshProvider>
|
|
@@ -191,10 +177,7 @@ describe('ComlinkTokenRefresh', () => {
|
|
|
191
177
|
mockFetch.mockRejectedValueOnce(new Error('Fetch failed'))
|
|
192
178
|
|
|
193
179
|
render(
|
|
194
|
-
<ResourceProvider
|
|
195
|
-
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
196
|
-
fallback={null}
|
|
197
|
-
>
|
|
180
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
198
181
|
<ComlinkTokenRefreshProvider>
|
|
199
182
|
<div>Test</div>
|
|
200
183
|
</ComlinkTokenRefreshProvider>
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import {type ClientError} from '@sanity/client'
|
|
2
2
|
import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
3
|
+
import {AuthStateType, getIsInDashboardState, setAuthToken} from '@sanity/sdk'
|
|
4
|
+
import {isStudioConfig} from '@sanity/sdk/_internal'
|
|
3
5
|
import {
|
|
4
|
-
AuthStateType,
|
|
5
6
|
type FrameMessage,
|
|
6
|
-
getIsInDashboardState,
|
|
7
|
-
isStudioConfig,
|
|
8
7
|
type NewTokenResponseMessage,
|
|
9
8
|
type RequestNewTokenMessage,
|
|
10
|
-
setAuthToken,
|
|
11
9
|
type WindowMessage,
|
|
12
|
-
} from '@sanity/sdk'
|
|
10
|
+
} from '@sanity/sdk/comlink'
|
|
13
11
|
import React, {type PropsWithChildren, useCallback, useEffect, useMemo, useRef} from 'react'
|
|
14
12
|
|
|
15
13
|
import {useAuthState} from '../hooks/auth/useAuthState'
|
|
@@ -2,9 +2,9 @@ import {type DocumentResource} from '@sanity/sdk'
|
|
|
2
2
|
import {createContext} from 'react'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Provides the active
|
|
6
|
-
* Set by
|
|
7
|
-
*
|
|
5
|
+
* Provides the active DocumentResource for a subtree.
|
|
6
|
+
* Set by ResourceProvider; read by useNormalizedResourceOptions as a fallback
|
|
7
|
+
* when hooks receive no explicit resource or resourceName.
|
|
8
8
|
* @internal
|
|
9
9
|
*/
|
|
10
10
|
export const ResourceContext = createContext<DocumentResource | undefined>(undefined)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {createSanityInstance, type DocumentResource} from '@sanity/sdk'
|
|
2
|
+
import {act, render} from '@testing-library/react'
|
|
3
|
+
import {type ReactNode, Suspense, useContext, useEffect} from 'react'
|
|
4
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {useDashboardOrganizationId} from '../hooks/auth/useDashboardOrganizationId'
|
|
7
|
+
import {resolveOrgResources} from '../utils/resolveOrgResources'
|
|
8
|
+
import {OrganizationResourcesProvider} from './OrganizationResourcesProvider'
|
|
9
|
+
import {ResourcesContext} from './ResourcesContext'
|
|
10
|
+
|
|
11
|
+
function promiseWithResolvers<T>(): {
|
|
12
|
+
promise: Promise<T>
|
|
13
|
+
resolve: (value: T) => void
|
|
14
|
+
} {
|
|
15
|
+
let resolve!: (value: T) => void
|
|
16
|
+
const promise = new Promise<T>((res) => {
|
|
17
|
+
resolve = res
|
|
18
|
+
})
|
|
19
|
+
return {promise, resolve}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Reset to a fresh instance per test so the module-level WeakMap cache (keyed by
|
|
23
|
+
// instance) starts empty. `useSanityInstance` returns the same object across
|
|
24
|
+
// renders within a test, which is what the cache relies on.
|
|
25
|
+
let instance = createSanityInstance()
|
|
26
|
+
|
|
27
|
+
vi.mock('../hooks/context/useSanityInstance', () => {
|
|
28
|
+
return {useSanityInstance: () => instance}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
vi.mock('../hooks/auth/useDashboardOrganizationId', () => ({
|
|
32
|
+
useDashboardOrganizationId: vi.fn(),
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
vi.mock('../utils/resolveOrgResources', () => ({
|
|
36
|
+
resolveOrgResources: vi.fn(),
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
const mockResolveOrgResources = vi.mocked(resolveOrgResources)
|
|
40
|
+
const mockUseDashboardOrganizationId = vi.mocked(useDashboardOrganizationId)
|
|
41
|
+
|
|
42
|
+
describe('OrganizationResourcesProvider', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks()
|
|
45
|
+
instance = createSanityInstance()
|
|
46
|
+
mockResolveOrgResources.mockResolvedValue({})
|
|
47
|
+
mockUseDashboardOrganizationId.mockReturnValue(undefined)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Captures ResourcesContext via useEffect (side-effect territory, not render)
|
|
51
|
+
// to satisfy the react-compiler purity rule.
|
|
52
|
+
function makeCapture() {
|
|
53
|
+
const deferred = promiseWithResolvers<Record<string, DocumentResource>>()
|
|
54
|
+
|
|
55
|
+
const CaptureContexts = () => {
|
|
56
|
+
const resources = useContext(ResourcesContext)
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
deferred.resolve(resources)
|
|
59
|
+
}, [resources])
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {CaptureContexts, capturedResources: deferred.promise}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderProvider(
|
|
67
|
+
children: ReactNode,
|
|
68
|
+
options?: {
|
|
69
|
+
resources?: Record<string, DocumentResource>
|
|
70
|
+
inferMediaLibraryAndCanvas?: boolean
|
|
71
|
+
},
|
|
72
|
+
) {
|
|
73
|
+
return render(
|
|
74
|
+
<Suspense fallback={<div>loading</div>}>
|
|
75
|
+
<OrganizationResourcesProvider
|
|
76
|
+
resources={options?.resources}
|
|
77
|
+
inferMediaLibraryAndCanvas={options?.inferMediaLibraryAndCanvas}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</OrganizationResourcesProvider>
|
|
81
|
+
</Suspense>,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
it('does not suspend when inferMediaLibraryAndCanvas is not set', async () => {
|
|
86
|
+
const {CaptureContexts, capturedResources} = makeCapture()
|
|
87
|
+
renderProvider(<CaptureContexts />)
|
|
88
|
+
const resources = await capturedResources
|
|
89
|
+
expect(Object.keys(resources)).toHaveLength(0)
|
|
90
|
+
expect(mockResolveOrgResources).not.toHaveBeenCalled()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('suspends children while org resources are being fetched, then renders with resolved resources', async () => {
|
|
94
|
+
mockUseDashboardOrganizationId.mockReturnValue('org-suspend-test')
|
|
95
|
+
const {promise, resolve} = promiseWithResolvers<void>()
|
|
96
|
+
mockResolveOrgResources.mockReturnValue(
|
|
97
|
+
promise.then(() => ({
|
|
98
|
+
mediaLibrary: {mediaLibraryId: 'ml-id'},
|
|
99
|
+
canvas: {canvasId: 'canvas-id'},
|
|
100
|
+
})) as ReturnType<typeof mockResolveOrgResources>,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const {CaptureContexts, capturedResources} = makeCapture()
|
|
104
|
+
await act(async () => {
|
|
105
|
+
renderProvider(<CaptureContexts />, {inferMediaLibraryAndCanvas: true})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await act(async () => {
|
|
109
|
+
resolve()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const resources = await capturedResources
|
|
113
|
+
expect(resources['media-library']).toEqual({mediaLibraryId: 'ml-id'})
|
|
114
|
+
expect(resources['canvas']).toEqual({canvasId: 'canvas-id'})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('renders without inferred entries and logs a warning when resolveOrgResources rejects', async () => {
|
|
118
|
+
mockUseDashboardOrganizationId.mockReturnValue('org-warn-test')
|
|
119
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
120
|
+
mockResolveOrgResources.mockRejectedValue(new Error('fetch failed'))
|
|
121
|
+
|
|
122
|
+
const {CaptureContexts, capturedResources} = makeCapture()
|
|
123
|
+
await act(async () => {
|
|
124
|
+
renderProvider(<CaptureContexts />, {inferMediaLibraryAndCanvas: true})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const resources = await capturedResources
|
|
128
|
+
expect(resources['media-library']).toBeUndefined()
|
|
129
|
+
expect(resources['canvas']).toBeUndefined()
|
|
130
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
131
|
+
'[sanity/sdk] Failed to infer org resources:',
|
|
132
|
+
expect.any(Error),
|
|
133
|
+
)
|
|
134
|
+
consoleSpy.mockRestore()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('caches Promise references across providers for the same instance', () => {
|
|
138
|
+
mockUseDashboardOrganizationId.mockReturnValue('org-cache-test')
|
|
139
|
+
render(
|
|
140
|
+
<Suspense fallback={null}>
|
|
141
|
+
<OrganizationResourcesProvider inferMediaLibraryAndCanvas>
|
|
142
|
+
{null}
|
|
143
|
+
</OrganizationResourcesProvider>
|
|
144
|
+
</Suspense>,
|
|
145
|
+
)
|
|
146
|
+
render(
|
|
147
|
+
<Suspense fallback={null}>
|
|
148
|
+
<OrganizationResourcesProvider inferMediaLibraryAndCanvas>
|
|
149
|
+
{null}
|
|
150
|
+
</OrganizationResourcesProvider>
|
|
151
|
+
</Suspense>,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// resolveOrgResources should only have been called once across both providers.
|
|
155
|
+
expect(mockResolveOrgResources).toHaveBeenCalledTimes(1)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('explicit resources are passed through to ResourcesContext', async () => {
|
|
159
|
+
const explicitResources = {
|
|
160
|
+
default: {projectId: 'p1', dataset: 'prod'},
|
|
161
|
+
secondary: {projectId: 'p2', dataset: 'staging'},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const {CaptureContexts, capturedResources} = makeCapture()
|
|
165
|
+
renderProvider(<CaptureContexts />, {resources: explicitResources})
|
|
166
|
+
|
|
167
|
+
expect(await capturedResources).toEqual(explicitResources)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('explicit resources take precedence over inferred ones', async () => {
|
|
171
|
+
mockUseDashboardOrganizationId.mockReturnValue('org-override-test')
|
|
172
|
+
mockResolveOrgResources.mockResolvedValue({
|
|
173
|
+
mediaLibrary: {mediaLibraryId: 'inferred-ml'},
|
|
174
|
+
canvas: {canvasId: 'inferred-canvas'},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const explicitResources = {'media-library': {mediaLibraryId: 'explicit-ml'}}
|
|
178
|
+
const {CaptureContexts, capturedResources} = makeCapture()
|
|
179
|
+
await act(async () => {
|
|
180
|
+
renderProvider(<CaptureContexts />, {
|
|
181
|
+
resources: explicitResources,
|
|
182
|
+
inferMediaLibraryAndCanvas: true,
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const resources = await capturedResources
|
|
187
|
+
expect(resources['media-library']).toEqual({mediaLibraryId: 'explicit-ml'})
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {type DocumentResource, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {type ReactElement, type ReactNode, use, useMemo} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useDashboardOrganizationId} from '../hooks/auth/useDashboardOrganizationId'
|
|
5
|
+
import {useSanityInstance} from '../hooks/context/useSanityInstance'
|
|
6
|
+
import {resolveOrgResources} from '../utils/resolveOrgResources'
|
|
7
|
+
import {ResourcesContext} from './ResourcesContext'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MEDIA_LIBRARY_RESOURCE_NAME = 'media-library'
|
|
10
|
+
const DEFAULT_CANVAS_RESOURCE_NAME = 'canvas'
|
|
11
|
+
|
|
12
|
+
type OrgResource = typeof DEFAULT_MEDIA_LIBRARY_RESOURCE_NAME | typeof DEFAULT_CANVAS_RESOURCE_NAME
|
|
13
|
+
type OrgResourcePromises = Map<OrgResource, PromiseLike<DocumentResource | undefined>>
|
|
14
|
+
|
|
15
|
+
// Module-level cache keyed by SanityInstance so Promise references are stable
|
|
16
|
+
// across Suspense unmount/remount cycles. React's use() tracks promises by
|
|
17
|
+
// identity, so stable references prevent unnecessary re-suspensions.
|
|
18
|
+
// WeakMap entries are GC'd when the instance is disposed.
|
|
19
|
+
// There is only ever one org at a time, so the instance alone is a sufficient
|
|
20
|
+
// key — organizationId only scopes the fetch, it doesn't need to key the cache.
|
|
21
|
+
const inferredResourceCache = new WeakMap<SanityInstance, OrgResourcePromises>()
|
|
22
|
+
|
|
23
|
+
function getOrgResourcePromises(
|
|
24
|
+
instance: SanityInstance,
|
|
25
|
+
organizationId: string,
|
|
26
|
+
): OrgResourcePromises {
|
|
27
|
+
let promises = inferredResourceCache.get(instance)
|
|
28
|
+
if (!promises) {
|
|
29
|
+
const basePromise = resolveOrgResources(instance, organizationId).then(
|
|
30
|
+
(result) => result,
|
|
31
|
+
(error) => {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.warn('[sanity/sdk] Failed to infer org resources:', error)
|
|
34
|
+
return {mediaLibrary: undefined, canvas: undefined}
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
promises = new Map([
|
|
38
|
+
[DEFAULT_MEDIA_LIBRARY_RESOURCE_NAME, basePromise.then((r) => r.mediaLibrary)],
|
|
39
|
+
[DEFAULT_CANVAS_RESOURCE_NAME, basePromise.then((r) => r.canvas)],
|
|
40
|
+
])
|
|
41
|
+
inferredResourceCache.set(instance, promises)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return promises
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Inner component that suspends via use() until both inference promises settle.
|
|
49
|
+
* Requires a Suspense boundary from the caller (usually a top ResourceProvider).
|
|
50
|
+
*/
|
|
51
|
+
function InferredResourcesProvider({
|
|
52
|
+
instance,
|
|
53
|
+
orgId,
|
|
54
|
+
explicitResources,
|
|
55
|
+
children,
|
|
56
|
+
}: {
|
|
57
|
+
instance: SanityInstance
|
|
58
|
+
orgId: string
|
|
59
|
+
explicitResources: Record<string, DocumentResource>
|
|
60
|
+
children: ReactNode
|
|
61
|
+
}): ReactElement {
|
|
62
|
+
const promises = getOrgResourcePromises(instance, orgId)
|
|
63
|
+
const ml = use(promises.get(DEFAULT_MEDIA_LIBRARY_RESOURCE_NAME)!)
|
|
64
|
+
const canvas = use(promises.get(DEFAULT_CANVAS_RESOURCE_NAME)!)
|
|
65
|
+
|
|
66
|
+
const resources = useMemo(() => {
|
|
67
|
+
const inferred: Record<string, DocumentResource> = {}
|
|
68
|
+
if (ml !== undefined) inferred[DEFAULT_MEDIA_LIBRARY_RESOURCE_NAME] = ml
|
|
69
|
+
if (canvas !== undefined) inferred[DEFAULT_CANVAS_RESOURCE_NAME] = canvas
|
|
70
|
+
return {...inferred, ...explicitResources}
|
|
71
|
+
}, [ml, canvas, explicitResources])
|
|
72
|
+
|
|
73
|
+
return <ResourcesContext.Provider value={resources}>{children}</ResourcesContext.Provider>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* When `inferMediaLibraryAndCanvas` is set, this component suspends until
|
|
78
|
+
* inference resolves. The nearest Suspense boundary (e.g. from ResourceProvider)
|
|
79
|
+
* shows its fallback during that window.
|
|
80
|
+
|
|
81
|
+
* If a user explicitly names a 'media-library' or 'canvas' resource,
|
|
82
|
+
* this component will use that resource instead of inferring it.
|
|
83
|
+
*/
|
|
84
|
+
export function OrganizationResourcesProvider({
|
|
85
|
+
resources: explicitResources = {},
|
|
86
|
+
inferMediaLibraryAndCanvas,
|
|
87
|
+
children,
|
|
88
|
+
}: {
|
|
89
|
+
resources?: Record<string, DocumentResource>
|
|
90
|
+
inferMediaLibraryAndCanvas?: boolean
|
|
91
|
+
children: ReactNode
|
|
92
|
+
}): ReactElement {
|
|
93
|
+
const instance = useSanityInstance()
|
|
94
|
+
const orgId = useDashboardOrganizationId()
|
|
95
|
+
|
|
96
|
+
if (!inferMediaLibraryAndCanvas || !orgId) {
|
|
97
|
+
return (
|
|
98
|
+
<ResourcesContext.Provider value={explicitResources}>{children}</ResourcesContext.Provider>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<InferredResourcesProvider
|
|
104
|
+
instance={instance}
|
|
105
|
+
orgId={orgId}
|
|
106
|
+
explicitResources={explicitResources}
|
|
107
|
+
>
|
|
108
|
+
{children}
|
|
109
|
+
</InferredResourcesProvider>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -2,9 +2,9 @@ import {type PerspectiveHandle} from '@sanity/sdk'
|
|
|
2
2
|
import {createContext} from 'react'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Provides
|
|
6
|
-
* Set by
|
|
7
|
-
*
|
|
5
|
+
* Provides the active perspective for a subtree.
|
|
6
|
+
* Set by ResourceProvider; injected by useNormalizedResourceOptions when
|
|
7
|
+
* the hook's options don't include an explicit perspective.
|
|
8
8
|
* @internal
|
|
9
9
|
*/
|
|
10
10
|
export const PerspectiveContext = createContext<PerspectiveHandle['perspective'] | undefined>(
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {createContext} from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carries a `projectId` for a subtree even when a complete
|
|
5
|
+
* `DatasetResource` can't be formed — e.g. a `<ResourceProvider projectId="…">`
|
|
6
|
+
* with no `dataset` and no parent resource to inherit one from.
|
|
7
|
+
*
|
|
8
|
+
* Project-scoped hooks (`useProject`, `useDatasets`, `useUsers`) read this as a
|
|
9
|
+
* fallback so they can still resolve a project in that case (the only hooks that would work with a config like this.)
|
|
10
|
+
*
|
|
11
|
+
* @remarks Temporary bridge for the dataset-less project scope.
|
|
12
|
+
* Remove in the next major version; those hooks should just always use a projectId or default to a resource available.
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export const ProjectContext = createContext<string | undefined>(undefined)
|