@sanity/sdk-react 0.0.0-alpha.21 → 0.0.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +502 -3460
- package/dist/index.js +400 -465
- package/dist/index.js.map +1 -1
- package/package.json +17 -15
- package/src/_exports/index.ts +4 -5
- package/src/components/SDKProvider.test.tsx +78 -54
- package/src/components/SDKProvider.tsx +31 -26
- package/src/components/SanityApp.test.tsx +121 -15
- package/src/components/SanityApp.tsx +26 -15
- package/src/components/auth/AuthBoundary.test.tsx +32 -14
- package/src/components/auth/AuthBoundary.tsx +53 -23
- package/src/components/auth/LoginCallback.test.tsx +19 -6
- package/src/components/auth/LoginCallback.tsx +2 -11
- package/src/components/auth/LoginError.test.tsx +12 -4
- package/src/components/auth/LoginError.tsx +13 -21
- package/src/components/auth/LoginFooter.test.tsx +7 -3
- package/src/context/ResourceProvider.test.tsx +157 -0
- package/src/context/ResourceProvider.tsx +111 -0
- package/src/context/SanityInstanceContext.ts +1 -1
- package/src/hooks/auth/useLoginUrl.tsx +14 -0
- package/src/hooks/client/useClient.ts +2 -1
- package/src/hooks/comlink/useManageFavorite.test.ts +16 -8
- package/src/hooks/comlink/useManageFavorite.ts +37 -13
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +8 -4
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +10 -8
- package/src/hooks/context/useSanityInstance.test.tsx +157 -15
- package/src/hooks/context/useSanityInstance.ts +66 -26
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +13 -31
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +12 -15
- package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.test.tsx → useStudioWorkspacesByProjectIdDataset.test.tsx} +13 -13
- package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.ts → useStudioWorkspacesByProjectIdDataset.ts} +10 -9
- package/src/hooks/datasets/useDatasets.ts +15 -4
- package/src/hooks/document/useApplyDocumentActions.test.ts +4 -9
- package/src/hooks/document/useApplyDocumentActions.ts +6 -31
- package/src/hooks/document/useDocument.test.ts +2 -2
- package/src/hooks/document/useDocument.ts +40 -19
- package/src/hooks/document/useDocumentEvent.test.ts +2 -3
- package/src/hooks/document/useDocumentEvent.ts +7 -11
- package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
- package/src/hooks/document/useDocumentPermissions.ts +31 -23
- package/src/hooks/document/useDocumentSyncStatus.ts +5 -4
- package/src/hooks/document/useEditDocument.test.ts +2 -3
- package/src/hooks/document/useEditDocument.ts +43 -29
- package/src/hooks/documents/useDocuments.test.tsx +30 -3
- package/src/hooks/documents/useDocuments.ts +20 -7
- package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
- package/src/hooks/helpers/createCallbackHook.tsx +2 -3
- package/src/hooks/helpers/createStateSourceHook.test.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +5 -8
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +43 -18
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +36 -50
- package/src/hooks/preview/usePreview.test.tsx +66 -7
- package/src/hooks/preview/usePreview.tsx +17 -12
- package/src/hooks/projection/useProjection.test.tsx +68 -3
- package/src/hooks/projection/useProjection.ts +21 -24
- package/src/hooks/projects/useProject.ts +7 -4
- package/src/hooks/query/useQuery.ts +32 -14
- package/src/hooks/users/useUsers.test.tsx +330 -0
- package/src/hooks/users/useUsers.ts +65 -52
- package/src/components/Login/LoginLinks.test.tsx +0 -90
- package/src/components/Login/LoginLinks.tsx +0 -58
- package/src/components/auth/Login.test.tsx +0 -27
- package/src/components/auth/Login.tsx +0 -39
- package/src/components/auth/LoginLayout.test.tsx +0 -19
- package/src/components/auth/LoginLayout.tsx +0 -69
- package/src/components/auth/authTestHelpers.tsx +0 -11
- package/src/context/SanityProvider.test.tsx +0 -25
- package/src/context/SanityProvider.tsx +0 -50
- package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
- package/src/hooks/auth/useLoginUrls.tsx +0 -52
- package/src/hooks/users/useUsers.test.ts +0 -163
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {Suspense, use, useEffect, useMemo, useRef} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_FALLBACK = (
|
|
7
|
+
<>
|
|
8
|
+
Warning: No fallback provided. Please supply a fallback prop to ensure proper Suspense handling.
|
|
9
|
+
</>
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Props for the ResourceProvider component
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export interface ResourceProviderProps extends SanityConfig {
|
|
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 a Sanity instance to child components through React Context
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* The ResourceProvider creates a hierarchical structure of Sanity instances:
|
|
32
|
+
* - When used as a root provider, it creates a new Sanity instance with the given config
|
|
33
|
+
* - When nested inside another ResourceProvider, it creates a child instance that
|
|
34
|
+
* inherits and extends the parent's configuration
|
|
35
|
+
*
|
|
36
|
+
* Features:
|
|
37
|
+
* - Automatically manages the lifecycle of Sanity instances
|
|
38
|
+
* - Disposes instances when the component unmounts
|
|
39
|
+
* - Includes a Suspense boundary for data loading
|
|
40
|
+
* - Enables hierarchical configuration inheritance
|
|
41
|
+
*
|
|
42
|
+
* Use this component to:
|
|
43
|
+
* - Set up project/dataset configuration for an application
|
|
44
|
+
* - Override specific configuration values in a section of your app
|
|
45
|
+
* - Create isolated instance hierarchies for different features
|
|
46
|
+
*
|
|
47
|
+
* @example Creating a root provider
|
|
48
|
+
* ```tsx
|
|
49
|
+
* <ResourceProvider
|
|
50
|
+
* projectId="your-project-id"
|
|
51
|
+
* dataset="production"
|
|
52
|
+
* fallback={<LoadingSpinner />}
|
|
53
|
+
* >
|
|
54
|
+
* <YourApp />
|
|
55
|
+
* </ResourceProvider>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example Creating nested providers with configuration inheritance
|
|
59
|
+
* ```tsx
|
|
60
|
+
* // Root provider with production config with nested provider for preview features with custom dataset
|
|
61
|
+
* <ResourceProvider projectId="abc123" dataset="production" fallback={<Loading />}>
|
|
62
|
+
* <div>...Main app content</div>
|
|
63
|
+
* <Dashboard />
|
|
64
|
+
* <ResourceProvider dataset="preview" fallback={<Loading />}>
|
|
65
|
+
* <PreviewFeatures />
|
|
66
|
+
* </ResourceProvider>
|
|
67
|
+
* </ResourceProvider>
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function ResourceProvider({
|
|
71
|
+
children,
|
|
72
|
+
fallback,
|
|
73
|
+
...config
|
|
74
|
+
}: ResourceProviderProps): React.ReactNode {
|
|
75
|
+
const parent = use(SanityInstanceContext)
|
|
76
|
+
const instance = useMemo(
|
|
77
|
+
() => (parent ? parent.createChild(config) : createSanityInstance(config)),
|
|
78
|
+
[config, parent],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Ref to hold the scheduled disposal timer.
|
|
82
|
+
const disposal = useRef<{
|
|
83
|
+
instance: SanityInstance
|
|
84
|
+
timeoutId: ReturnType<typeof setTimeout>
|
|
85
|
+
} | null>(null)
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
// If the component remounts quickly (as in Strict Mode), cancel any pending disposal.
|
|
89
|
+
if (disposal.current !== null && instance === disposal.current.instance) {
|
|
90
|
+
clearTimeout(disposal.current.timeoutId)
|
|
91
|
+
disposal.current = null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
disposal.current = {
|
|
96
|
+
instance,
|
|
97
|
+
timeoutId: setTimeout(() => {
|
|
98
|
+
if (!instance.isDisposed()) {
|
|
99
|
+
instance.dispose()
|
|
100
|
+
}
|
|
101
|
+
}, 0),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, [instance])
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<SanityInstanceContext.Provider value={instance}>
|
|
108
|
+
<Suspense fallback={fallback ?? DEFAULT_FALLBACK}>{children}</Suspense>
|
|
109
|
+
</SanityInstanceContext.Provider>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {getLoginUrlState} from '@sanity/sdk'
|
|
2
|
+
import {useMemo, useSyncExternalStore} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export function useLoginUrl(): string {
|
|
10
|
+
const instance = useSanityInstance()
|
|
11
|
+
const {subscribe, getCurrent} = useMemo(() => getLoginUrlState(instance), [instance])
|
|
12
|
+
|
|
13
|
+
return useSyncExternalStore(subscribe, getCurrent as () => string)
|
|
14
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {getClientState} from '@sanity/sdk'
|
|
2
|
+
import {identity} from 'rxjs'
|
|
2
3
|
|
|
3
4
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
4
5
|
|
|
@@ -31,5 +32,5 @@ import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
|
31
32
|
*/
|
|
32
33
|
export const useClient = createStateSourceHook({
|
|
33
34
|
getState: getClientState,
|
|
34
|
-
|
|
35
|
+
getConfig: identity,
|
|
35
36
|
})
|
|
@@ -57,11 +57,15 @@ describe('useManageFavorite', () => {
|
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
expect(node.post).toHaveBeenCalledWith('dashboard/v1/events/favorite/mutate', {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
document: {
|
|
61
|
+
id: 'mock-id',
|
|
62
|
+
type: 'mock-type',
|
|
63
|
+
resource: {
|
|
64
|
+
id: 'test.test',
|
|
65
|
+
type: 'studio',
|
|
66
|
+
},
|
|
67
|
+
},
|
|
62
68
|
eventType: 'added',
|
|
63
|
-
resourceType: 'studio',
|
|
64
|
-
resourceId: undefined,
|
|
65
69
|
})
|
|
66
70
|
expect(result.current.isFavorited).toBe(true)
|
|
67
71
|
})
|
|
@@ -74,11 +78,15 @@ describe('useManageFavorite', () => {
|
|
|
74
78
|
})
|
|
75
79
|
|
|
76
80
|
expect(node.post).toHaveBeenCalledWith('dashboard/v1/events/favorite/mutate', {
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
document: {
|
|
82
|
+
id: 'mock-id',
|
|
83
|
+
type: 'mock-type',
|
|
84
|
+
resource: {
|
|
85
|
+
id: 'test.test',
|
|
86
|
+
type: 'studio',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
79
89
|
eventType: 'removed',
|
|
80
|
-
resourceType: 'studio',
|
|
81
|
-
resourceId: undefined,
|
|
82
90
|
})
|
|
83
91
|
expect(result.current.isFavorited).toBe(false)
|
|
84
92
|
})
|
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
SDK_NODE_NAME,
|
|
8
8
|
type StudioResource,
|
|
9
9
|
} from '@sanity/message-protocol'
|
|
10
|
-
import {type FrameMessage} from '@sanity/sdk'
|
|
11
|
-
import {useCallback, useState} from 'react'
|
|
10
|
+
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
11
|
+
import {useCallback, useEffect, useState} from 'react'
|
|
12
12
|
|
|
13
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
13
14
|
import {useWindowConnection} from './useWindowConnection'
|
|
14
15
|
|
|
15
16
|
// should we import this whole type from the message protocol?
|
|
@@ -21,9 +22,7 @@ interface ManageFavorite {
|
|
|
21
22
|
isConnected: boolean
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
interface UseManageFavoriteProps {
|
|
25
|
-
documentId: string
|
|
26
|
-
documentType: string
|
|
25
|
+
interface UseManageFavoriteProps extends DocumentHandle {
|
|
27
26
|
resourceId?: string
|
|
28
27
|
resourceType: StudioResource['type'] | MediaResource['type'] | CanvasResource['type']
|
|
29
28
|
}
|
|
@@ -64,21 +63,43 @@ interface UseManageFavoriteProps {
|
|
|
64
63
|
export function useManageFavorite({
|
|
65
64
|
documentId,
|
|
66
65
|
documentType,
|
|
67
|
-
|
|
66
|
+
projectId: paramProjectId,
|
|
67
|
+
dataset: paramDataset,
|
|
68
|
+
resourceId: paramResourceId,
|
|
68
69
|
resourceType,
|
|
69
70
|
}: UseManageFavoriteProps): ManageFavorite {
|
|
70
71
|
const [isFavorited, setIsFavorited] = useState(false) // should load this from a comlink fetch
|
|
71
72
|
const [status, setStatus] = useState<Status>('idle')
|
|
73
|
+
const [resourceId, setResourceId] = useState<string>(paramResourceId || '')
|
|
72
74
|
const {sendMessage} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
|
|
73
75
|
name: SDK_NODE_NAME,
|
|
74
76
|
connectTo: SDK_CHANNEL_NAME,
|
|
75
77
|
onStatus: setStatus,
|
|
76
78
|
})
|
|
79
|
+
const instance = useSanityInstance()
|
|
80
|
+
const {config} = instance
|
|
81
|
+
const instanceProjectId = config?.projectId
|
|
82
|
+
const instanceDataset = config?.dataset
|
|
83
|
+
const projectId = paramProjectId ?? instanceProjectId
|
|
84
|
+
const dataset = paramDataset ?? instanceDataset
|
|
77
85
|
|
|
78
|
-
if (resourceType
|
|
79
|
-
throw new Error('
|
|
86
|
+
if (resourceType === 'studio' && (!projectId || !dataset)) {
|
|
87
|
+
throw new Error('projectId and dataset are required for studio resources')
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
// If resourceType is studio and the resourceId is not provided,
|
|
92
|
+
// use the projectId and dataset to generate a resourceId
|
|
93
|
+
if (resourceType === 'studio' && !paramResourceId) {
|
|
94
|
+
setResourceId(`${projectId}.${dataset}`)
|
|
95
|
+
} else if (paramResourceId) {
|
|
96
|
+
setResourceId(paramResourceId)
|
|
97
|
+
} else {
|
|
98
|
+
// For other resource types, resourceId is required
|
|
99
|
+
throw new Error('resourceId is required for media-library and canvas resources')
|
|
100
|
+
}
|
|
101
|
+
}, [resourceType, paramResourceId, projectId, dataset])
|
|
102
|
+
|
|
82
103
|
const handleFavoriteAction = useCallback(
|
|
83
104
|
(action: 'added' | 'removed', setFavoriteState: boolean) => {
|
|
84
105
|
if (!documentId || !documentType || !resourceType) return
|
|
@@ -88,11 +109,14 @@ export function useManageFavorite({
|
|
|
88
109
|
type: 'dashboard/v1/events/favorite/mutate',
|
|
89
110
|
data: {
|
|
90
111
|
eventType: action,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
112
|
+
document: {
|
|
113
|
+
id: documentId,
|
|
114
|
+
type: documentType,
|
|
115
|
+
resource: {
|
|
116
|
+
id: resourceId,
|
|
117
|
+
type: resourceType,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
96
120
|
},
|
|
97
121
|
response: {
|
|
98
122
|
success: true,
|
|
@@ -62,10 +62,14 @@ describe('useRecordDocumentHistoryEvent', () => {
|
|
|
62
62
|
|
|
63
63
|
expect(node.post).toHaveBeenCalledWith('dashboard/v1/events/history', {
|
|
64
64
|
eventType: 'viewed',
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
document: {
|
|
66
|
+
id: 'mock-id',
|
|
67
|
+
type: 'mock-type',
|
|
68
|
+
resource: {
|
|
69
|
+
id: 'mock-resource-id',
|
|
70
|
+
type: 'studio',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
69
73
|
})
|
|
70
74
|
})
|
|
71
75
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
SDK_NODE_NAME,
|
|
8
8
|
type StudioResource,
|
|
9
9
|
} from '@sanity/message-protocol'
|
|
10
|
-
import {type FrameMessage} from '@sanity/sdk'
|
|
10
|
+
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
11
11
|
import {useCallback, useState} from 'react'
|
|
12
12
|
|
|
13
13
|
import {useWindowConnection} from './useWindowConnection'
|
|
@@ -20,9 +20,7 @@ interface DocumentInteractionHistory {
|
|
|
20
20
|
/**
|
|
21
21
|
* @public
|
|
22
22
|
*/
|
|
23
|
-
interface UseRecordDocumentHistoryEventProps {
|
|
24
|
-
documentId: string
|
|
25
|
-
documentType: string
|
|
23
|
+
interface UseRecordDocumentHistoryEventProps extends DocumentHandle {
|
|
26
24
|
resourceType: StudioResource['type'] | MediaResource['type'] | CanvasResource['type']
|
|
27
25
|
resourceId?: string
|
|
28
26
|
}
|
|
@@ -82,10 +80,14 @@ export function useRecordDocumentHistoryEvent({
|
|
|
82
80
|
type: 'dashboard/v1/events/history',
|
|
83
81
|
data: {
|
|
84
82
|
eventType,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
document: {
|
|
84
|
+
id: documentId,
|
|
85
|
+
type: documentType,
|
|
86
|
+
resource: {
|
|
87
|
+
id: resourceId!,
|
|
88
|
+
type: resourceType,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
89
91
|
},
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -1,31 +1,173 @@
|
|
|
1
|
-
import {createSanityInstance} from '@sanity/sdk'
|
|
1
|
+
import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {renderHook} from '@testing-library/react'
|
|
3
|
-
import
|
|
4
|
-
import {describe, expect, it
|
|
3
|
+
import {type ReactNode} from 'react'
|
|
4
|
+
import {describe, expect, it} from 'vitest'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
7
7
|
import {useSanityInstance} from './useSanityInstance'
|
|
8
8
|
|
|
9
9
|
describe('useSanityInstance', () => {
|
|
10
|
-
|
|
10
|
+
function createWrapper(instance: SanityInstance | null) {
|
|
11
|
+
return function Wrapper({children}: {children: ReactNode}) {
|
|
12
|
+
return (
|
|
13
|
+
<SanityInstanceContext.Provider value={instance}>{children}</SanityInstanceContext.Provider>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
11
17
|
|
|
12
|
-
it('
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
it('should return the Sanity instance from context', () => {
|
|
19
|
+
// Create a Sanity instance
|
|
20
|
+
const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
|
|
21
|
+
|
|
22
|
+
// Render the hook with the wrapper that provides the context
|
|
23
|
+
const {result} = renderHook(() => useSanityInstance(), {
|
|
24
|
+
wrapper: createWrapper(instance),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Check that the correct instance is returned
|
|
28
|
+
expect(result.current).toBe(instance)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should throw an error if no instance is found in context', () => {
|
|
32
|
+
// Expect the hook to throw when no instance is in context
|
|
33
|
+
expect(() => {
|
|
34
|
+
renderHook(() => useSanityInstance(), {
|
|
35
|
+
wrapper: createWrapper(null),
|
|
36
|
+
})
|
|
37
|
+
}).toThrow('SanityInstance context not found')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should include the requested config in error message when no instance found', () => {
|
|
41
|
+
const requestedConfig = {projectId: 'test', dataset: 'test'}
|
|
42
|
+
|
|
43
|
+
// Expect the hook to throw and include the requested config in the error
|
|
44
|
+
expect(() => {
|
|
45
|
+
renderHook(() => useSanityInstance(requestedConfig), {
|
|
46
|
+
wrapper: createWrapper(null),
|
|
47
|
+
})
|
|
48
|
+
}).toThrow(JSON.stringify(requestedConfig, null, 2))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should find a matching instance with provided config', () => {
|
|
52
|
+
// Create a parent instance
|
|
53
|
+
const parentInstance = createSanityInstance({
|
|
54
|
+
projectId: 'parent-project',
|
|
55
|
+
dataset: 'parent-dataset',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Create a child instance
|
|
59
|
+
const childInstance = parentInstance.createChild({dataset: 'child-dataset'})
|
|
60
|
+
|
|
61
|
+
// Render the hook with the child instance and request the parent config
|
|
62
|
+
const {result} = renderHook(
|
|
63
|
+
() => useSanityInstance({projectId: 'parent-project', dataset: 'parent-dataset'}),
|
|
64
|
+
{wrapper: createWrapper(childInstance)},
|
|
15
65
|
)
|
|
16
66
|
|
|
17
|
-
|
|
67
|
+
// Should match and return the parent instance
|
|
68
|
+
expect(result.current).toBe(parentInstance)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should throw an error if no matching instance is found for config', () => {
|
|
72
|
+
// Create an instance
|
|
73
|
+
const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
|
|
18
74
|
|
|
19
|
-
|
|
75
|
+
// Request a config that doesn't match
|
|
76
|
+
const requestedConfig: SanityConfig = {
|
|
77
|
+
projectId: 'non-existent',
|
|
78
|
+
dataset: 'not-found',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Expect the hook to throw for a non-matching config
|
|
82
|
+
expect(() => {
|
|
83
|
+
renderHook(() => useSanityInstance(requestedConfig), {
|
|
84
|
+
wrapper: createWrapper(instance),
|
|
85
|
+
})
|
|
86
|
+
}).toThrow('Could not find a matching Sanity instance')
|
|
20
87
|
})
|
|
21
88
|
|
|
22
|
-
it('
|
|
23
|
-
const
|
|
89
|
+
it('should include the requested config in error message when no matching instance', () => {
|
|
90
|
+
const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
|
|
91
|
+
const requestedConfig = {projectId: 'different', dataset: 'different'}
|
|
24
92
|
|
|
93
|
+
// Expect the error to include the requested config details
|
|
25
94
|
expect(() => {
|
|
26
|
-
renderHook(() => useSanityInstance()
|
|
27
|
-
|
|
95
|
+
renderHook(() => useSanityInstance(requestedConfig), {
|
|
96
|
+
wrapper: createWrapper(instance),
|
|
97
|
+
})
|
|
98
|
+
}).toThrow(JSON.stringify(requestedConfig, null, 2))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should return the current instance when no config is provided', () => {
|
|
102
|
+
// Create a hierarchy of instances
|
|
103
|
+
const grandparent = createSanityInstance({projectId: 'gp', dataset: 'gp-ds'})
|
|
104
|
+
const parent = grandparent.createChild({projectId: 'p'})
|
|
105
|
+
const child = parent.createChild({dataset: 'child-ds'})
|
|
106
|
+
|
|
107
|
+
// Render the hook with the child instance and no config
|
|
108
|
+
const {result} = renderHook(() => useSanityInstance(), {
|
|
109
|
+
wrapper: createWrapper(child),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Should return the child instance
|
|
113
|
+
expect(result.current).toBe(child)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should match child instance when it satisfies the config', () => {
|
|
117
|
+
// Create a parent instance
|
|
118
|
+
const parent = createSanityInstance({projectId: 'parent', dataset: 'parent-ds'})
|
|
119
|
+
|
|
120
|
+
// Create a child instance that inherits projectId
|
|
121
|
+
const child = parent.createChild({dataset: 'child-ds'})
|
|
122
|
+
|
|
123
|
+
// Render the hook with the child instance and request by the child's dataset
|
|
124
|
+
const {result} = renderHook(() => useSanityInstance({dataset: 'child-ds'}), {
|
|
125
|
+
wrapper: createWrapper(child),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Should match and return the child instance
|
|
129
|
+
expect(result.current).toBe(child)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should match partial config correctly', () => {
|
|
133
|
+
// Create an instance with multiple config values
|
|
134
|
+
const instance = createSanityInstance({
|
|
135
|
+
projectId: 'test-proj',
|
|
136
|
+
dataset: 'test-ds',
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Should match when requesting just one property
|
|
140
|
+
const {result} = renderHook(() => useSanityInstance({dataset: 'test-ds'}), {
|
|
141
|
+
wrapper: createWrapper(instance),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(result.current).toBe(instance)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("should match deeper in hierarchy when current instance doesn't match", () => {
|
|
148
|
+
// Create a three-level hierarchy
|
|
149
|
+
const root = createSanityInstance({projectId: 'root', dataset: 'root-ds'})
|
|
150
|
+
const middle = root.createChild({projectId: 'middle'})
|
|
151
|
+
const leaf = middle.createChild({dataset: 'leaf-ds'})
|
|
152
|
+
|
|
153
|
+
// Request config matching the root from the leaf
|
|
154
|
+
const {result} = renderHook(() => useSanityInstance({projectId: 'root', dataset: 'root-ds'}), {
|
|
155
|
+
wrapper: createWrapper(leaf),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Should find and return the root instance
|
|
159
|
+
expect(result.current).toBe(root)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should match undefined values in config', () => {
|
|
163
|
+
// Create instance with only projectId
|
|
164
|
+
const rootInstance = createSanityInstance({projectId: 'test'})
|
|
165
|
+
|
|
166
|
+
// Match specifically looking for undefined dataset
|
|
167
|
+
const {result} = renderHook(() => useSanityInstance({dataset: undefined}), {
|
|
168
|
+
wrapper: createWrapper(rootInstance),
|
|
169
|
+
})
|
|
28
170
|
|
|
29
|
-
|
|
171
|
+
expect(result.current).toBe(rootInstance)
|
|
30
172
|
})
|
|
31
173
|
})
|
|
@@ -1,39 +1,79 @@
|
|
|
1
|
-
import {type SanityInstance} from '@sanity/sdk'
|
|
2
|
-
import {
|
|
1
|
+
import {type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {use} from 'react'
|
|
3
3
|
|
|
4
4
|
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* This must be called from within a `SanityProvider` component.
|
|
9
|
-
* @internal
|
|
7
|
+
* Retrieves the current Sanity instance or finds a matching instance from the hierarchy
|
|
10
8
|
*
|
|
11
|
-
* @
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
9
|
+
* @public
|
|
10
|
+
*
|
|
11
|
+
* @param config - Optional configuration to match against when finding an instance
|
|
12
|
+
* @returns The current or matching Sanity instance
|
|
13
|
+
*
|
|
14
|
+
* @remarks
|
|
15
|
+
* This hook accesses the nearest Sanity instance from the React context. When provided with
|
|
16
|
+
* a configuration object, it traverses up the instance hierarchy to find the closest instance
|
|
17
|
+
* that matches the specified configuration using shallow comparison of properties.
|
|
18
|
+
*
|
|
19
|
+
* The hook must be used within a component wrapped by a `ResourceProvider` or `SanityApp`.
|
|
20
|
+
*
|
|
21
|
+
* Use this hook when you need to:
|
|
22
|
+
* - Access the current SanityInstance from context
|
|
23
|
+
* - Find a specific instance with matching project/dataset configuration
|
|
24
|
+
* - Access a parent instance with specific configuration values
|
|
25
|
+
*
|
|
26
|
+
* @example Get the current instance
|
|
27
|
+
* ```tsx
|
|
28
|
+
* // Get the current instance from context
|
|
29
|
+
* const instance = useSanityInstance()
|
|
30
|
+
* console.log(instance.config.projectId)
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example Find an instance with specific configuration
|
|
34
|
+
* ```tsx
|
|
35
|
+
* // Find an instance matching the given project and dataset
|
|
36
|
+
* const instance = useSanityInstance({
|
|
37
|
+
* projectId: 'abc123',
|
|
38
|
+
* dataset: 'production'
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* // Use instance for API calls
|
|
42
|
+
* const fetchDocument = (docId) => {
|
|
43
|
+
* // Instance is guaranteed to have the matching config
|
|
44
|
+
* return client.fetch(`*[_id == $id][0]`, { id: docId })
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example Match partial configuration
|
|
14
49
|
* ```tsx
|
|
15
|
-
*
|
|
50
|
+
* // Find an instance with specific auth configuration
|
|
51
|
+
* const instance = useSanityInstance({
|
|
52
|
+
* auth: { requireLogin: true }
|
|
53
|
+
* })
|
|
16
54
|
* ```
|
|
55
|
+
*
|
|
56
|
+
* @throws Error if no SanityInstance is found in context
|
|
57
|
+
* @throws Error if no matching instance is found for the provided config
|
|
17
58
|
*/
|
|
18
|
-
export const useSanityInstance = (
|
|
19
|
-
const
|
|
20
|
-
if (!sanityInstance) {
|
|
21
|
-
throw new Error('useSanityInstance must be called from within the SanityProvider')
|
|
22
|
-
}
|
|
23
|
-
if (sanityInstance.length === 0) {
|
|
24
|
-
throw new Error('No Sanity instances found')
|
|
25
|
-
}
|
|
26
|
-
if (sanityInstance.length === 1 || !resourceId) {
|
|
27
|
-
return sanityInstance[0]
|
|
28
|
-
}
|
|
59
|
+
export const useSanityInstance = (config?: SanityConfig): SanityInstance => {
|
|
60
|
+
const instance = use(SanityInstanceContext)
|
|
29
61
|
|
|
30
|
-
if (!
|
|
31
|
-
throw new Error(
|
|
62
|
+
if (!instance) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`SanityInstance context not found. ${config ? `Requested config: ${JSON.stringify(config, null, 2)}. ` : ''}Please ensure that your component is wrapped in a <ResourceProvider> or a <SanityApp>.`,
|
|
65
|
+
)
|
|
32
66
|
}
|
|
33
67
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
if (!config) return instance
|
|
69
|
+
|
|
70
|
+
const match = instance.match(config)
|
|
71
|
+
if (!match) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Could not find a matching Sanity instance for the requested configuration: ${JSON.stringify(config, null, 2)}.
|
|
74
|
+
Please ensure there is a <ResourceProvider> with a matching configuration in the component hierarchy.`,
|
|
75
|
+
)
|
|
37
76
|
}
|
|
38
|
-
|
|
77
|
+
|
|
78
|
+
return match
|
|
39
79
|
}
|