@sanity/sdk-react 2.5.0 → 2.6.0
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 +97 -20
- package/dist/index.d.ts +35 -14
- package/dist/index.js +84 -44
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/_exports/sdk-react.ts +1 -0
- package/src/components/SDKProvider.tsx +8 -3
- package/src/components/SanityApp.tsx +2 -1
- package/src/context/SourcesContext.tsx +7 -0
- package/src/context/renderSanityApp.test.tsx +355 -0
- package/src/context/renderSanityApp.tsx +48 -0
- package/src/hooks/context/useSource.tsx +34 -0
- package/src/hooks/dashboard/useDispatchIntent.test.ts +24 -18
- package/src/hooks/dashboard/useDispatchIntent.ts +9 -10
- package/src/hooks/dashboard/utils/{getResourceIdFromDocumentHandle.test.ts → useResourceIdFromDocumentHandle.test.ts} +33 -59
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +46 -0
- package/src/hooks/document/useEditDocument.ts +3 -0
- package/src/hooks/documents/useDocuments.ts +3 -2
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +1 -0
- package/src/hooks/query/useQuery.ts +21 -8
- package/src/hooks/dashboard/types.ts +0 -12
- package/src/hooks/dashboard/utils/getResourceIdFromDocumentHandle.ts +0 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -45,14 +45,14 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@sanity/client": "^7.12.0",
|
|
47
47
|
"@sanity/message-protocol": "^0.18.0",
|
|
48
|
-
"@sanity/types": "^
|
|
48
|
+
"@sanity/types": "^5.2.0",
|
|
49
49
|
"@types/lodash-es": "^4.17.12",
|
|
50
50
|
"groq": "3.88.1-typegen-experimental.0",
|
|
51
51
|
"lodash-es": "^4.17.21",
|
|
52
52
|
"react-compiler-runtime": "19.1.0-rc.2",
|
|
53
53
|
"react-error-boundary": "^5.0.0",
|
|
54
54
|
"rxjs": "^7.8.2",
|
|
55
|
-
"@sanity/sdk": "2.
|
|
55
|
+
"@sanity/sdk": "2.6.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@sanity/browserslist-config": "^1.0.5",
|
|
@@ -79,9 +79,9 @@
|
|
|
79
79
|
"vitest": "^3.2.4",
|
|
80
80
|
"@repo/config-eslint": "0.0.0",
|
|
81
81
|
"@repo/config-test": "0.0.1",
|
|
82
|
+
"@repo/package.bundle": "3.82.0",
|
|
82
83
|
"@repo/package.config": "0.0.1",
|
|
83
|
-
"@repo/tsconfig": "0.0.1"
|
|
84
|
-
"@repo/package.bundle": "3.82.0"
|
|
84
|
+
"@repo/tsconfig": "0.0.1"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -5,6 +5,7 @@ export {AuthBoundary, type AuthBoundaryProps} from '../components/auth/AuthBound
|
|
|
5
5
|
export {SanityApp, type SanityAppProps} from '../components/SanityApp'
|
|
6
6
|
export {SDKProvider, type SDKProviderProps} from '../components/SDKProvider'
|
|
7
7
|
export {ComlinkTokenRefreshProvider} from '../context/ComlinkTokenRefresh'
|
|
8
|
+
export {renderSanityApp} from '../context/renderSanityApp'
|
|
8
9
|
export {ResourceProvider, type ResourceProviderProps} from '../context/ResourceProvider'
|
|
9
10
|
export {
|
|
10
11
|
useAgentGenerate,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {type SanityConfig} from '@sanity/sdk'
|
|
2
|
-
import {type ReactElement, type ReactNode} from 'react'
|
|
1
|
+
import {type DocumentSource, type SanityConfig} from '@sanity/sdk'
|
|
2
|
+
import {type ReactElement, type ReactNode, useMemo} from 'react'
|
|
3
3
|
|
|
4
4
|
import {ResourceProvider} from '../context/ResourceProvider'
|
|
5
|
+
import {SourcesContext} from '../context/SourcesContext'
|
|
5
6
|
import {AuthBoundary, type AuthBoundaryProps} from './auth/AuthBoundary'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -11,6 +12,7 @@ export interface SDKProviderProps extends AuthBoundaryProps {
|
|
|
11
12
|
children: ReactNode
|
|
12
13
|
config: SanityConfig | SanityConfig[]
|
|
13
14
|
fallback: ReactNode
|
|
15
|
+
sources?: Record<string, DocumentSource>
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -31,12 +33,15 @@ export function SDKProvider({
|
|
|
31
33
|
const configs = (Array.isArray(config) ? config : [config]).slice().reverse()
|
|
32
34
|
const projectIds = configs.map((c) => c.projectId).filter((id): id is string => !!id)
|
|
33
35
|
|
|
36
|
+
// Memoize sources to prevent creating a new empty object on every render
|
|
37
|
+
const sourcesValue = useMemo(() => props.sources ?? {}, [props.sources])
|
|
38
|
+
|
|
34
39
|
// Create a nested structure of ResourceProviders for each config
|
|
35
40
|
const createNestedProviders = (index: number): ReactElement => {
|
|
36
41
|
if (index >= configs.length) {
|
|
37
42
|
return (
|
|
38
43
|
<AuthBoundary {...props} projectIds={projectIds}>
|
|
39
|
-
{children}
|
|
44
|
+
<SourcesContext.Provider value={sourcesValue}>{children}</SourcesContext.Provider>
|
|
40
45
|
</AuthBoundary>
|
|
41
46
|
)
|
|
42
47
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type SanityConfig} from '@sanity/sdk'
|
|
1
|
+
import {type DocumentSource, type SanityConfig} from '@sanity/sdk'
|
|
2
2
|
import {type ReactElement, useEffect} from 'react'
|
|
3
3
|
|
|
4
4
|
import {SDKProvider} from './SDKProvider'
|
|
@@ -13,6 +13,7 @@ export interface SanityAppProps {
|
|
|
13
13
|
config: SanityConfig | SanityConfig[]
|
|
14
14
|
/** @deprecated use the `config` prop instead. */
|
|
15
15
|
sanityConfigs?: SanityConfig[]
|
|
16
|
+
sources?: Record<string, DocumentSource>
|
|
16
17
|
children: React.ReactNode
|
|
17
18
|
/* Fallback content to show when child components are suspending. Same as the `fallback` prop for React Suspense. */
|
|
18
19
|
fallback: React.ReactNode
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import {type SanityConfig} from '@sanity/sdk'
|
|
2
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {SanityApp} from '../components/SanityApp'
|
|
5
|
+
import {renderSanityApp} from './renderSanityApp'
|
|
6
|
+
|
|
7
|
+
// Hoist the mock functions
|
|
8
|
+
const mockRender = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockUnmount = vi.hoisted(() => vi.fn())
|
|
10
|
+
const mockCreateRoot = vi.hoisted(() =>
|
|
11
|
+
vi.fn(() => ({
|
|
12
|
+
render: mockRender,
|
|
13
|
+
unmount: mockUnmount,
|
|
14
|
+
})),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// Mock the SanityApp component
|
|
18
|
+
vi.mock('../components/SanityApp', () => ({
|
|
19
|
+
SanityApp: vi.fn(({children}) => <div data-testid="sanity-app">{children}</div>),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
// Mock react-dom/client
|
|
23
|
+
vi.mock('react-dom/client', () => ({
|
|
24
|
+
createRoot: mockCreateRoot,
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
describe('renderSanityApp', () => {
|
|
28
|
+
let rootElement: HTMLElement | null
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks()
|
|
32
|
+
mockRender.mockClear()
|
|
33
|
+
mockUnmount.mockClear()
|
|
34
|
+
mockCreateRoot.mockClear()
|
|
35
|
+
rootElement = document.createElement('div')
|
|
36
|
+
document.body.appendChild(rootElement)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
if (rootElement && rootElement.parentNode) {
|
|
41
|
+
document.body.removeChild(rootElement)
|
|
42
|
+
}
|
|
43
|
+
rootElement = null
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('throws error when rootElement is null', () => {
|
|
47
|
+
const namedSources = {
|
|
48
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
expect(() => renderSanityApp(null, namedSources, {}, <div>Test</div>)).toThrowError(
|
|
52
|
+
'Missing root element to mount application into',
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('creates root with the provided element', () => {
|
|
57
|
+
const namedSources = {
|
|
58
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
62
|
+
|
|
63
|
+
expect(mockCreateRoot).toHaveBeenCalledWith(rootElement)
|
|
64
|
+
expect(mockCreateRoot).toHaveBeenCalledTimes(1)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('converts namedSources object to array of configs', () => {
|
|
68
|
+
const namedSources = {
|
|
69
|
+
main: {projectId: 'project-1', dataset: 'production'},
|
|
70
|
+
secondary: {projectId: 'project-2', dataset: 'staging'},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
74
|
+
|
|
75
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
76
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
77
|
+
expect(renderCall).toBeDefined()
|
|
78
|
+
|
|
79
|
+
// The renderCall is the SanityApp component directly when not using StrictMode
|
|
80
|
+
expect(renderCall.type).toBe(SanityApp)
|
|
81
|
+
expect(renderCall.props.config).toEqual([
|
|
82
|
+
{projectId: 'project-1', dataset: 'production'},
|
|
83
|
+
{projectId: 'project-2', dataset: 'staging'},
|
|
84
|
+
])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('renders without StrictMode when reactStrictMode is false', () => {
|
|
88
|
+
const namedSources = {
|
|
89
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
90
|
+
}
|
|
91
|
+
const children = <div>Test Children</div>
|
|
92
|
+
|
|
93
|
+
renderSanityApp(rootElement, namedSources, {reactStrictMode: false}, children)
|
|
94
|
+
|
|
95
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
96
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
97
|
+
|
|
98
|
+
// Should not have StrictMode wrapper
|
|
99
|
+
expect(renderCall.type).toBe(SanityApp)
|
|
100
|
+
expect(renderCall.props.children).toEqual(children)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('renders without StrictMode by default', () => {
|
|
104
|
+
const namedSources = {
|
|
105
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
106
|
+
}
|
|
107
|
+
const children = <div>Test Children</div>
|
|
108
|
+
|
|
109
|
+
renderSanityApp(rootElement, namedSources, {}, children)
|
|
110
|
+
|
|
111
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
112
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
113
|
+
|
|
114
|
+
// Should not have StrictMode wrapper when not specified
|
|
115
|
+
expect(renderCall.type).toBe(SanityApp)
|
|
116
|
+
expect(renderCall.props.children).toEqual(children)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('renders with StrictMode when reactStrictMode is true', () => {
|
|
120
|
+
const namedSources = {
|
|
121
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
122
|
+
}
|
|
123
|
+
const children = <div>Test Children</div>
|
|
124
|
+
|
|
125
|
+
renderSanityApp(rootElement, namedSources, {reactStrictMode: true}, children)
|
|
126
|
+
|
|
127
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
128
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
129
|
+
|
|
130
|
+
// Should have StrictMode wrapper (StrictMode is a Symbol in React 18)
|
|
131
|
+
expect(renderCall.type).toBeDefined()
|
|
132
|
+
expect(renderCall.type.toString()).toContain('Symbol')
|
|
133
|
+
const strictModeChild = renderCall.props.children
|
|
134
|
+
expect(strictModeChild.type).toBe(SanityApp)
|
|
135
|
+
expect(strictModeChild.props.children).toEqual(children)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('passes loading fallback to SanityApp', () => {
|
|
139
|
+
const namedSources = {
|
|
140
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
144
|
+
|
|
145
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
146
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
147
|
+
const sanityAppElement = renderCall
|
|
148
|
+
|
|
149
|
+
expect(sanityAppElement.type).toBe(SanityApp)
|
|
150
|
+
expect(sanityAppElement.props.fallback).toEqual(<div>Loading...</div>)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('returns an unmount function', () => {
|
|
154
|
+
const namedSources = {
|
|
155
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const unmount = renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
159
|
+
|
|
160
|
+
expect(typeof unmount).toBe('function')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('calls root.unmount when unmount function is invoked', () => {
|
|
164
|
+
const namedSources = {
|
|
165
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const unmount = renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
169
|
+
|
|
170
|
+
expect(mockUnmount).not.toHaveBeenCalled()
|
|
171
|
+
unmount()
|
|
172
|
+
expect(mockUnmount).toHaveBeenCalledTimes(1)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('handles empty namedSources object', () => {
|
|
176
|
+
const namedSources = {}
|
|
177
|
+
|
|
178
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
179
|
+
|
|
180
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
181
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
182
|
+
const sanityAppElement = renderCall
|
|
183
|
+
|
|
184
|
+
expect(sanityAppElement.type).toBe(SanityApp)
|
|
185
|
+
expect(sanityAppElement.props.config).toEqual([])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('handles single namedSource', () => {
|
|
189
|
+
const namedSources = {
|
|
190
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
194
|
+
|
|
195
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
196
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
197
|
+
const sanityAppElement = renderCall
|
|
198
|
+
|
|
199
|
+
expect(sanityAppElement.type).toBe(SanityApp)
|
|
200
|
+
expect(sanityAppElement.props.config).toEqual([
|
|
201
|
+
{projectId: 'test-project', dataset: 'production'},
|
|
202
|
+
])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('handles multiple namedSources', () => {
|
|
206
|
+
const namedSources = {
|
|
207
|
+
main: {projectId: 'project-1', dataset: 'production'},
|
|
208
|
+
blog: {projectId: 'project-2', dataset: 'staging'},
|
|
209
|
+
ecommerce: {projectId: 'project-3', dataset: 'development'},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
213
|
+
|
|
214
|
+
expect(mockRender).toHaveBeenCalledTimes(1)
|
|
215
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
216
|
+
const sanityAppElement = renderCall
|
|
217
|
+
|
|
218
|
+
expect(sanityAppElement.type).toBe(SanityApp)
|
|
219
|
+
expect(sanityAppElement.props.config).toHaveLength(3)
|
|
220
|
+
expect(sanityAppElement.props.config).toEqual([
|
|
221
|
+
{projectId: 'project-1', dataset: 'production'},
|
|
222
|
+
{projectId: 'project-2', dataset: 'staging'},
|
|
223
|
+
{projectId: 'project-3', dataset: 'development'},
|
|
224
|
+
])
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('preserves order of namedSources in config array', () => {
|
|
228
|
+
const namedSources = {
|
|
229
|
+
z: {projectId: 'project-z', dataset: 'z-dataset'},
|
|
230
|
+
a: {projectId: 'project-a', dataset: 'a-dataset'},
|
|
231
|
+
m: {projectId: 'project-m', dataset: 'm-dataset'},
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
renderSanityApp(rootElement, namedSources, {}, <div>Test</div>)
|
|
235
|
+
|
|
236
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
237
|
+
const sanityAppElement = renderCall
|
|
238
|
+
|
|
239
|
+
// Object.values preserves insertion order in modern JS
|
|
240
|
+
expect(sanityAppElement.props.config).toEqual([
|
|
241
|
+
{projectId: 'project-z', dataset: 'z-dataset'},
|
|
242
|
+
{projectId: 'project-a', dataset: 'a-dataset'},
|
|
243
|
+
{projectId: 'project-m', dataset: 'm-dataset'},
|
|
244
|
+
])
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('passes children to SanityApp', () => {
|
|
248
|
+
const namedSources = {
|
|
249
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
250
|
+
}
|
|
251
|
+
const children = (
|
|
252
|
+
<div>
|
|
253
|
+
<h1>Test App</h1>
|
|
254
|
+
<p>Content</p>
|
|
255
|
+
</div>
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
renderSanityApp(rootElement, namedSources, {}, children)
|
|
259
|
+
|
|
260
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
261
|
+
const sanityAppElement = renderCall
|
|
262
|
+
|
|
263
|
+
expect(sanityAppElement.props.children).toEqual(children)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('works with different types of children', () => {
|
|
267
|
+
const namedSources = {
|
|
268
|
+
main: {projectId: 'test-project', dataset: 'production'},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Test with string children
|
|
272
|
+
renderSanityApp(rootElement, namedSources, {}, 'String child')
|
|
273
|
+
|
|
274
|
+
let renderCall = mockRender.mock.calls[0][0]
|
|
275
|
+
let sanityAppElement = renderCall
|
|
276
|
+
|
|
277
|
+
expect(sanityAppElement.props.children).toBe('String child')
|
|
278
|
+
|
|
279
|
+
// Test with null children
|
|
280
|
+
mockRender.mockClear()
|
|
281
|
+
renderSanityApp(rootElement, namedSources, {}, null)
|
|
282
|
+
|
|
283
|
+
renderCall = mockRender.mock.calls[0][0]
|
|
284
|
+
sanityAppElement = renderCall
|
|
285
|
+
|
|
286
|
+
expect(sanityAppElement.props.children).toBe(null)
|
|
287
|
+
|
|
288
|
+
// Test with array of children
|
|
289
|
+
mockRender.mockClear()
|
|
290
|
+
const arrayChildren = [<div key="1">Child 1</div>, <div key="2">Child 2</div>]
|
|
291
|
+
renderSanityApp(rootElement, namedSources, {}, arrayChildren)
|
|
292
|
+
|
|
293
|
+
renderCall = mockRender.mock.calls[0][0]
|
|
294
|
+
sanityAppElement = renderCall
|
|
295
|
+
|
|
296
|
+
expect(sanityAppElement.props.children).toEqual(arrayChildren)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('integrates with StrictMode and passes all props correctly', () => {
|
|
300
|
+
const namedSources = {
|
|
301
|
+
main: {
|
|
302
|
+
projectId: 'test-project',
|
|
303
|
+
dataset: 'production',
|
|
304
|
+
apiVersion: '2023-01-01',
|
|
305
|
+
} as SanityConfig,
|
|
306
|
+
secondary: {
|
|
307
|
+
projectId: 'test-project-2',
|
|
308
|
+
dataset: 'staging',
|
|
309
|
+
} as SanityConfig,
|
|
310
|
+
}
|
|
311
|
+
const children = <div>App Content</div>
|
|
312
|
+
|
|
313
|
+
renderSanityApp(rootElement, namedSources, {reactStrictMode: true}, children)
|
|
314
|
+
|
|
315
|
+
const renderCall = mockRender.mock.calls[0][0]
|
|
316
|
+
|
|
317
|
+
// Verify StrictMode wrapper (StrictMode is a Symbol in React 18)
|
|
318
|
+
expect(renderCall.type).toBeDefined()
|
|
319
|
+
expect(renderCall.type.toString()).toContain('Symbol')
|
|
320
|
+
|
|
321
|
+
// Verify SanityApp is inside StrictMode
|
|
322
|
+
const strictModeChild = renderCall.props.children
|
|
323
|
+
expect(strictModeChild.type).toBe(SanityApp)
|
|
324
|
+
|
|
325
|
+
// Verify all props are passed correctly
|
|
326
|
+
expect(strictModeChild.props.config).toEqual([
|
|
327
|
+
{projectId: 'test-project', dataset: 'production', apiVersion: '2023-01-01'},
|
|
328
|
+
{projectId: 'test-project-2', dataset: 'staging'},
|
|
329
|
+
])
|
|
330
|
+
expect(strictModeChild.props.fallback).toEqual(<div>Loading...</div>)
|
|
331
|
+
expect(strictModeChild.props.children).toEqual(children)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('can be called multiple times with different roots', () => {
|
|
335
|
+
const rootElement2 = document.createElement('div')
|
|
336
|
+
document.body.appendChild(rootElement2)
|
|
337
|
+
|
|
338
|
+
const namedSources1 = {main: {projectId: 'project-1', dataset: 'production'}}
|
|
339
|
+
const namedSources2 = {main: {projectId: 'project-2', dataset: 'staging'}}
|
|
340
|
+
|
|
341
|
+
const unmount1 = renderSanityApp(rootElement, namedSources1, {}, <div>App 1</div>)
|
|
342
|
+
const unmount2 = renderSanityApp(rootElement2, namedSources2, {}, <div>App 2</div>)
|
|
343
|
+
|
|
344
|
+
expect(mockCreateRoot).toHaveBeenCalledTimes(2)
|
|
345
|
+
expect(mockCreateRoot).toHaveBeenNthCalledWith(1, rootElement)
|
|
346
|
+
expect(mockCreateRoot).toHaveBeenNthCalledWith(2, rootElement2)
|
|
347
|
+
|
|
348
|
+
unmount1()
|
|
349
|
+
unmount2()
|
|
350
|
+
|
|
351
|
+
expect(mockUnmount).toHaveBeenCalledTimes(2)
|
|
352
|
+
|
|
353
|
+
document.body.removeChild(rootElement2)
|
|
354
|
+
})
|
|
355
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {type SanityConfig} from '@sanity/sdk'
|
|
2
|
+
import {StrictMode} from 'react'
|
|
3
|
+
import {createRoot} from 'react-dom/client'
|
|
4
|
+
|
|
5
|
+
import {SanityApp} from '../components/SanityApp'
|
|
6
|
+
|
|
7
|
+
interface RenderSanitySDKAppOptions {
|
|
8
|
+
reactStrictMode?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** In-flight CLI PR is using named sources since it's aspirational.
|
|
12
|
+
* We can transform the shape in this function until it's finalized.
|
|
13
|
+
*/
|
|
14
|
+
interface NamedSources {
|
|
15
|
+
[key: string]: SanityConfig
|
|
16
|
+
}
|
|
17
|
+
/** @internal */
|
|
18
|
+
export function renderSanityApp(
|
|
19
|
+
rootElement: HTMLElement | null,
|
|
20
|
+
namedSources: NamedSources,
|
|
21
|
+
options: RenderSanitySDKAppOptions,
|
|
22
|
+
children: React.ReactNode,
|
|
23
|
+
): () => void {
|
|
24
|
+
if (!rootElement) {
|
|
25
|
+
throw new Error('Missing root element to mount application into')
|
|
26
|
+
}
|
|
27
|
+
const {reactStrictMode = false} = options
|
|
28
|
+
|
|
29
|
+
const root = createRoot(rootElement)
|
|
30
|
+
const config = Object.values(namedSources)
|
|
31
|
+
|
|
32
|
+
root.render(
|
|
33
|
+
reactStrictMode ? (
|
|
34
|
+
<StrictMode>
|
|
35
|
+
{/* TODO: think about a loading component we want to be "universal" */}
|
|
36
|
+
<SanityApp config={config} fallback={<div>Loading...</div>}>
|
|
37
|
+
{children}
|
|
38
|
+
</SanityApp>
|
|
39
|
+
</StrictMode>
|
|
40
|
+
) : (
|
|
41
|
+
<SanityApp config={config} fallback={<div>Loading...</div>}>
|
|
42
|
+
{children}
|
|
43
|
+
</SanityApp>
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return () => root.unmount()
|
|
48
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {type DatasetHandle, type DocumentHandle, type DocumentSource} from '@sanity/sdk'
|
|
2
|
+
import {useContext} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SourcesContext} from '../../context/SourcesContext'
|
|
5
|
+
|
|
6
|
+
/** Retrieves the named source from context.
|
|
7
|
+
* @beta
|
|
8
|
+
* @param name - The name of the source to retrieve.
|
|
9
|
+
* @returns The source.
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const source = useSource('my-source')
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function useSource(options: DocumentHandle | DatasetHandle): DocumentSource | undefined {
|
|
16
|
+
const sources = useContext(SourcesContext)
|
|
17
|
+
|
|
18
|
+
// this might return the "default" source in the future once we implement it?
|
|
19
|
+
if (!options.sourceName && !options.source) {
|
|
20
|
+
return undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (options.source) {
|
|
24
|
+
return options.source
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (options.sourceName && !Object.hasOwn(sources, options.sourceName)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`There's no source named ${JSON.stringify(options.sourceName)} in context. Please use <SourceProvider>.`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return options.sourceName ? sources[options.sourceName] : undefined
|
|
34
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {renderHook} from '@testing-library/react'
|
|
1
|
+
import {type DocumentHandle} from '@sanity/sdk'
|
|
3
2
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import {renderHook} from '../../../test/test-utils'
|
|
6
5
|
import {useDispatchIntent} from './useDispatchIntent'
|
|
7
6
|
|
|
8
7
|
// Mock the useWindowConnection hook
|
|
@@ -62,9 +61,13 @@ describe('useDispatchIntent', () => {
|
|
|
62
61
|
})
|
|
63
62
|
|
|
64
63
|
it('should use memoized dispatchIntent function', () => {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
const params = {action: 'edit' as const, documentHandle: mockDocumentHandle}
|
|
65
|
+
const {result, rerender} = renderHook(
|
|
66
|
+
({params: hookParams}: {params: typeof params}) => useDispatchIntent(hookParams),
|
|
67
|
+
{
|
|
68
|
+
initialProps: {params},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
68
71
|
|
|
69
72
|
const firstDispatchIntent = result.current.dispatchIntent
|
|
70
73
|
|
|
@@ -75,9 +78,12 @@ describe('useDispatchIntent', () => {
|
|
|
75
78
|
})
|
|
76
79
|
|
|
77
80
|
it('should create new dispatchIntent function when documentHandle changes', () => {
|
|
78
|
-
const {result, rerender} = renderHook(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
const {result, rerender} = renderHook(
|
|
82
|
+
(params: {action: 'edit'; documentHandle: DocumentHandle}) => useDispatchIntent(params),
|
|
83
|
+
{
|
|
84
|
+
initialProps: {action: 'edit' as const, documentHandle: mockDocumentHandle},
|
|
85
|
+
},
|
|
86
|
+
)
|
|
81
87
|
|
|
82
88
|
const firstDispatchIntent = result.current.dispatchIntent
|
|
83
89
|
|
|
@@ -88,7 +94,7 @@ describe('useDispatchIntent', () => {
|
|
|
88
94
|
dataset: 'new-dataset',
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
rerender({
|
|
97
|
+
rerender({action: 'edit' as const, documentHandle: newDocumentHandle})
|
|
92
98
|
|
|
93
99
|
expect(result.current.dispatchIntent).not.toBe(firstDispatchIntent)
|
|
94
100
|
})
|
|
@@ -160,10 +166,10 @@ describe('useDispatchIntent', () => {
|
|
|
160
166
|
})
|
|
161
167
|
|
|
162
168
|
it('should send intent message with media library source', () => {
|
|
163
|
-
const mockMediaLibraryHandle:
|
|
169
|
+
const mockMediaLibraryHandle: DocumentHandle = {
|
|
164
170
|
documentId: 'test-asset-id',
|
|
165
171
|
documentType: 'sanity.asset',
|
|
166
|
-
|
|
172
|
+
sourceName: 'media-library',
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
const {result} = renderHook(() =>
|
|
@@ -182,17 +188,17 @@ describe('useDispatchIntent', () => {
|
|
|
182
188
|
type: 'sanity.asset',
|
|
183
189
|
},
|
|
184
190
|
resource: {
|
|
185
|
-
id: '
|
|
191
|
+
id: 'media-library-id',
|
|
186
192
|
type: 'media-library',
|
|
187
193
|
},
|
|
188
194
|
})
|
|
189
195
|
})
|
|
190
196
|
|
|
191
197
|
it('should send intent message with canvas source', () => {
|
|
192
|
-
const mockCanvasHandle:
|
|
198
|
+
const mockCanvasHandle: DocumentHandle = {
|
|
193
199
|
documentId: 'test-canvas-document-id',
|
|
194
200
|
documentType: 'sanity.canvas.document',
|
|
195
|
-
|
|
201
|
+
sourceName: 'canvas',
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
const {result} = renderHook(() =>
|
|
@@ -211,7 +217,7 @@ describe('useDispatchIntent', () => {
|
|
|
211
217
|
type: 'sanity.canvas.document',
|
|
212
218
|
},
|
|
213
219
|
resource: {
|
|
214
|
-
id: '
|
|
220
|
+
id: 'canvas-id',
|
|
215
221
|
type: 'canvas',
|
|
216
222
|
},
|
|
217
223
|
})
|
|
@@ -227,12 +233,12 @@ describe('useDispatchIntent', () => {
|
|
|
227
233
|
const {result} = renderHook(() =>
|
|
228
234
|
useDispatchIntent({
|
|
229
235
|
action: 'edit',
|
|
230
|
-
documentHandle: invalidHandle as unknown as
|
|
236
|
+
documentHandle: invalidHandle as unknown as DocumentHandle,
|
|
231
237
|
}),
|
|
232
238
|
)
|
|
233
239
|
|
|
234
240
|
expect(() => result.current.dispatchIntent()).toThrow(
|
|
235
|
-
'useDispatchIntent: Either `
|
|
241
|
+
'useDispatchIntent: Either `sourceName` or both `projectId` and `dataset` must be provided in documentHandle.',
|
|
236
242
|
)
|
|
237
243
|
})
|
|
238
244
|
})
|