@sanity/sdk-react 2.8.0 → 2.10.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.
Files changed (53) hide show
  1. package/dist/index.d.ts +232 -47
  2. package/dist/index.js +468 -263
  3. package/dist/index.js.map +1 -1
  4. package/package.json +8 -10
  5. package/src/_exports/sdk-react.ts +5 -0
  6. package/src/components/SDKProvider.tsx +36 -8
  7. package/src/components/SanityApp.tsx +3 -2
  8. package/src/components/auth/AuthBoundary.tsx +8 -1
  9. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  10. package/src/components/auth/LoginError.test.tsx +191 -5
  11. package/src/components/auth/LoginError.tsx +100 -56
  12. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  13. package/src/components/errors/ChunkLoadError.tsx +56 -0
  14. package/src/components/errors/chunkReloadStorage.ts +57 -0
  15. package/src/context/ResourceProvider.test.tsx +7 -1
  16. package/src/context/ResourceProvider.tsx +11 -4
  17. package/src/context/ResourcesContext.tsx +7 -0
  18. package/src/context/SDKStudioContext.ts +6 -0
  19. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  20. package/src/context/SanityInstanceProvider.tsx +71 -0
  21. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  22. package/src/hooks/dashboard/useDispatchIntent.test.ts +8 -6
  23. package/src/hooks/dashboard/useDispatchIntent.ts +6 -6
  24. package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
  25. package/src/hooks/dashboard/useWindowTitle.ts +112 -0
  26. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
  27. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -13
  28. package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
  29. package/src/hooks/document/useApplyDocumentActions.ts +99 -3
  30. package/src/hooks/document/useDocument.ts +22 -6
  31. package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
  32. package/src/hooks/document/useDocumentEvent.ts +10 -3
  33. package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
  34. package/src/hooks/document/useDocumentPermissions.ts +22 -0
  35. package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
  36. package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
  37. package/src/hooks/document/useEditDocument.ts +34 -8
  38. package/src/hooks/documents/useDocuments.ts +11 -6
  39. package/src/hooks/helpers/useNormalizedResourceOptions.ts +131 -0
  40. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  41. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +11 -8
  42. package/src/hooks/presence/usePresence.test.tsx +56 -9
  43. package/src/hooks/presence/usePresence.ts +25 -4
  44. package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
  45. package/src/hooks/preview/useDocumentPreview.tsx +40 -55
  46. package/src/hooks/projection/useDocumentProjection.ts +8 -6
  47. package/src/hooks/query/useQuery.ts +12 -9
  48. package/src/hooks/releases/useActiveReleases.ts +32 -13
  49. package/src/hooks/releases/usePerspective.ts +26 -14
  50. package/src/hooks/users/useUser.ts +2 -0
  51. package/src/hooks/users/useUsers.ts +2 -0
  52. package/src/context/SourcesContext.tsx +0 -7
  53. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -85
@@ -0,0 +1,59 @@
1
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
2
+
3
+ import {render, screen} from '../../../test/test-utils'
4
+ import {ChunkLoadError} from './ChunkLoadError'
5
+ import {CHUNK_RELOAD_STORAGE_KEY} from './chunkReloadStorage'
6
+
7
+ const noop = (): void => {}
8
+
9
+ describe('ChunkLoadError', () => {
10
+ const reloadSpy = vi.fn()
11
+ const originalLocation = window.location
12
+
13
+ beforeEach(() => {
14
+ reloadSpy.mockReset()
15
+ window.sessionStorage.clear()
16
+ Object.defineProperty(window, 'location', {
17
+ configurable: true,
18
+ value: {...originalLocation, reload: reloadSpy},
19
+ })
20
+ })
21
+
22
+ afterEach(() => {
23
+ Object.defineProperty(window, 'location', {configurable: true, value: originalLocation})
24
+ window.sessionStorage.clear()
25
+ })
26
+
27
+ it('triggers an automatic reload and renders nothing on the first occurrence', () => {
28
+ render(
29
+ <ChunkLoadError
30
+ error={new Error('Failed to fetch dynamically imported module')}
31
+ resetErrorBoundary={noop}
32
+ />,
33
+ )
34
+
35
+ expect(reloadSpy).toHaveBeenCalledTimes(1)
36
+ expect(window.sessionStorage.getItem(CHUNK_RELOAD_STORAGE_KEY)).toBe('1')
37
+ expect(screen.queryByText(/new version/i)).toBeNull()
38
+ })
39
+
40
+ it('renders the manual reload UI when the flag is already set', () => {
41
+ window.sessionStorage.setItem(CHUNK_RELOAD_STORAGE_KEY, '1')
42
+
43
+ render(
44
+ <ChunkLoadError
45
+ error={new Error('Failed to fetch dynamically imported module')}
46
+ resetErrorBoundary={noop}
47
+ />,
48
+ )
49
+
50
+ expect(reloadSpy).not.toHaveBeenCalled()
51
+ expect(screen.getByText('A new version is available')).toBeInTheDocument()
52
+
53
+ const button = screen.getByRole('button', {name: 'Reload page'})
54
+ button.click()
55
+
56
+ expect(reloadSpy).toHaveBeenCalledTimes(1)
57
+ expect(window.sessionStorage.getItem(CHUNK_RELOAD_STORAGE_KEY)).toBeNull()
58
+ })
59
+ })
@@ -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
+ }
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  import {type SanityConfig, type SanityInstance} from '@sanity/sdk'
2
2
  import {act, render, screen} from '@testing-library/react'
3
3
  import {StrictMode, use, useEffect} from 'react'
4
- import {describe, expect, it} from 'vitest'
4
+ import {describe, expect, it, vi} from 'vitest'
5
5
 
6
6
  import {ResourceProvider} from './ResourceProvider'
7
7
  import {SanityInstanceContext} from './SanityInstanceContext'
@@ -37,6 +37,7 @@ describe('ResourceProvider', () => {
37
37
  })
38
38
 
39
39
  it('shows fallback during loading', async () => {
40
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
40
41
  const {promise, resolve} = promiseWithResolvers()
41
42
  function SuspendingChild(): React.ReactNode {
42
43
  throw promise
@@ -52,6 +53,8 @@ describe('ResourceProvider', () => {
52
53
  act(() => {
53
54
  resolve()
54
55
  })
56
+ await new Promise((r) => setTimeout(r, 0))
57
+ consoleSpy.mockRestore()
55
58
  })
56
59
 
57
60
  it('creates root instance when no parent context exists', async () => {
@@ -141,6 +144,7 @@ describe('ResourceProvider', () => {
141
144
  })
142
145
 
143
146
  it('uses default fallback when none provided', async () => {
147
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
144
148
  const {promise, resolve} = promiseWithResolvers()
145
149
  function SuspendingChild(): React.ReactNode {
146
150
  throw promise
@@ -157,5 +161,7 @@ describe('ResourceProvider', () => {
157
161
  act(() => {
158
162
  resolve()
159
163
  })
164
+ await new Promise((r) => setTimeout(r, 0))
165
+ consoleSpy.mockRestore()
160
166
  })
161
167
  })
@@ -1,7 +1,9 @@
1
1
  import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
2
- import {Suspense, useContext, useEffect, useMemo, useRef} from 'react'
2
+ import {initTelemetry} from '@sanity/sdk/_internal'
3
+ import {useContext, useEffect, useMemo, useRef} from 'react'
3
4
 
4
5
  import {SanityInstanceContext} from './SanityInstanceContext'
6
+ import {SanityInstanceProvider} from './SanityInstanceProvider'
5
7
 
6
8
  const DEFAULT_FALLBACK = (
7
9
  <>
@@ -78,6 +80,11 @@ export function ResourceProvider({
78
80
  [config, parent],
79
81
  )
80
82
 
83
+ const projectId = config.projectId ?? ''
84
+ useMemo(() => {
85
+ if (projectId && !parent) initTelemetry(instance, projectId)
86
+ }, [instance, projectId, parent])
87
+
81
88
  // Ref to hold the scheduled disposal timer.
82
89
  const disposal = useRef<{
83
90
  instance: SanityInstance
@@ -104,8 +111,8 @@ export function ResourceProvider({
104
111
  }, [instance])
105
112
 
106
113
  return (
107
- <SanityInstanceContext.Provider value={instance}>
108
- <Suspense fallback={fallback ?? DEFAULT_FALLBACK}>{children}</Suspense>
109
- </SanityInstanceContext.Provider>
114
+ <SanityInstanceProvider instance={instance} fallback={fallback ?? DEFAULT_FALLBACK}>
115
+ {children}
116
+ </SanityInstanceProvider>
110
117
  )
111
118
  }
@@ -0,0 +1,7 @@
1
+ import {type DocumentResource} from '@sanity/sdk'
2
+ import {createContext} from 'react'
3
+
4
+ /** Context for resources.
5
+ * @beta
6
+ */
7
+ export const ResourcesContext = createContext<Record<string, DocumentResource>>({})
@@ -13,6 +13,12 @@ export interface StudioWorkspaceHandle {
13
13
  projectId: string
14
14
  /** The dataset name for this workspace. */
15
15
  dataset: string
16
+ /**
17
+ * Whether the Studio has determined the user is authenticated.
18
+ * When `true` and the token source emits `null`, the SDK infers
19
+ * cookie-based auth is in use and skips the logged-out state.
20
+ */
21
+ authenticated?: boolean
16
22
  /** Authentication state for this workspace. */
17
23
  auth: {
18
24
  /**
@@ -0,0 +1,100 @@
1
+ import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
2
+ import {act, render, screen} from '@testing-library/react'
3
+ import {use, useEffect} from 'react'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {SanityInstanceContext} from './SanityInstanceContext'
7
+ import {SanityInstanceProvider} from './SanityInstanceProvider'
8
+
9
+ function promiseWithResolvers<T = void>(): {
10
+ promise: Promise<T>
11
+ resolve: (t: T) => void
12
+ reject: (error: unknown) => void
13
+ } {
14
+ let resolve!: (t: T) => void
15
+ let reject!: (error: unknown) => void
16
+ const promise = new Promise<T>((res, rej) => {
17
+ resolve = res
18
+ reject = rej
19
+ })
20
+ return {resolve, reject, promise}
21
+ }
22
+
23
+ describe('SanityInstanceProvider', () => {
24
+ it('renders children', () => {
25
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
26
+
27
+ render(
28
+ <SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
29
+ <div data-testid="test-child">Child Component</div>
30
+ </SanityInstanceProvider>,
31
+ )
32
+
33
+ expect(screen.getByTestId('test-child')).toBeInTheDocument()
34
+ instance.dispose()
35
+ })
36
+
37
+ it('provides the given instance via context', async () => {
38
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
39
+ const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
40
+
41
+ const CaptureInstance = () => {
42
+ const ctx = use(SanityInstanceContext)
43
+ useEffect(() => resolve(ctx), [ctx])
44
+ return null
45
+ }
46
+
47
+ render(
48
+ <SanityInstanceProvider instance={instance} fallback={null}>
49
+ <CaptureInstance />
50
+ </SanityInstanceProvider>,
51
+ )
52
+
53
+ const provided = await promise
54
+ expect(provided).toBe(instance)
55
+ instance.dispose()
56
+ })
57
+
58
+ it('shows fallback during suspense', async () => {
59
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
60
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
61
+ const {promise, resolve} = promiseWithResolvers()
62
+
63
+ function SuspendingChild(): React.ReactNode {
64
+ throw promise
65
+ }
66
+
67
+ render(
68
+ <SanityInstanceProvider
69
+ instance={instance}
70
+ fallback={<div data-testid="fallback">Loading...</div>}
71
+ >
72
+ <SuspendingChild />
73
+ </SanityInstanceProvider>,
74
+ )
75
+
76
+ expect(screen.getByTestId('fallback')).toBeInTheDocument()
77
+ act(() => {
78
+ resolve()
79
+ })
80
+ await new Promise((r) => setTimeout(r, 0))
81
+ instance.dispose()
82
+ consoleSpy.mockRestore()
83
+ })
84
+
85
+ it('does not dispose the instance on unmount', async () => {
86
+ const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
87
+
88
+ const {unmount} = render(
89
+ <SanityInstanceProvider instance={instance} fallback={null}>
90
+ <div />
91
+ </SanityInstanceProvider>,
92
+ )
93
+
94
+ unmount()
95
+ await new Promise((r) => setTimeout(r, 0))
96
+
97
+ expect(instance.isDisposed()).toBe(false)
98
+ instance.dispose()
99
+ })
100
+ })
@@ -0,0 +1,71 @@
1
+ import {type SanityInstance} from '@sanity/sdk'
2
+ import {Suspense} from 'react'
3
+
4
+ import {SanityInstanceContext} from './SanityInstanceContext'
5
+
6
+ /**
7
+ * Props for the SanityInstanceProvider component
8
+ * @public
9
+ */
10
+ export interface SanityInstanceProviderProps {
11
+ /**
12
+ * A pre-created SanityInstance to provide to child components.
13
+ * The caller owns the instance lifecycle — SanityInstanceProvider
14
+ * will not dispose it on unmount.
15
+ */
16
+ instance: SanityInstance
17
+ /**
18
+ * React node to show while content is loading.
19
+ * Used as the fallback for the internal Suspense boundary.
20
+ */
21
+ fallback: React.ReactNode
22
+ children: React.ReactNode
23
+ }
24
+
25
+ /**
26
+ * Provides an externally-created Sanity instance to child components through React Context.
27
+ *
28
+ * @internal
29
+ *
30
+ * @remarks
31
+ * Unlike {@link ResourceProvider}, this component does not create or dispose a SanityInstance.
32
+ * The caller is responsible for creating the instance via `createSanityInstance` and disposing
33
+ * it when appropriate. This is useful when a non-React system layer (e.g. a state machine)
34
+ * owns the instance and the React tree should consume it without managing its lifecycle.
35
+ *
36
+ * All SDK hooks (`useSanityInstance`, `useDocuments`, etc.) will read from the provided instance.
37
+ *
38
+ * @example Providing a pre-created instance
39
+ * ```tsx
40
+ * import { createSanityInstance, type SanityConfig } from '@sanity/sdk'
41
+ * import { SanityInstanceProvider } from '@sanity/sdk-react'
42
+ *
43
+ * const config: SanityConfig = {
44
+ * projectId: 'my-project-id',
45
+ * dataset: 'production',
46
+ * }
47
+ *
48
+ * const instance = createSanityInstance(config)
49
+ *
50
+ * function App() {
51
+ * return (
52
+ * <SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
53
+ * <MyApp />
54
+ * </SanityInstanceProvider>
55
+ * )
56
+ * }
57
+ * ```
58
+ *
59
+ * @category Components
60
+ */
61
+ export function SanityInstanceProvider({
62
+ instance,
63
+ fallback,
64
+ children,
65
+ }: SanityInstanceProviderProps): React.ReactNode {
66
+ return (
67
+ <SanityInstanceContext.Provider value={instance}>
68
+ <Suspense fallback={fallback}>{children}</Suspense>
69
+ </SanityInstanceContext.Provider>
70
+ )
71
+ }
@@ -27,13 +27,20 @@ export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): s
27
27
  const instance = useSanityInstance()
28
28
  const [error, setError] = useState<string | null>(null)
29
29
 
30
+ const isInactive = disabled || !projectIds || projectIds.length === 0
31
+
32
+ // Reset stale errors when verification turns off so the next activation
33
+ // doesn't briefly leak the previous result.
34
+ const [prevInactive, setPrevInactive] = useState(isInactive)
35
+ if (prevInactive !== isInactive) {
36
+ setPrevInactive(isInactive)
37
+ if (isInactive) setError(null)
38
+ }
39
+
30
40
  useEffect(() => {
31
- if (disabled || !projectIds || projectIds.length === 0) {
32
- if (error !== null) setError(null)
33
- return
34
- }
41
+ if (isInactive) return
35
42
 
36
- const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds)
43
+ const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds!)
37
44
 
38
45
  const subscription = verificationObservable$.subscribe((result: OrgVerificationResult) => {
39
46
  setError(result.error)
@@ -42,7 +49,7 @@ export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): s
42
49
  return () => {
43
50
  subscription.unsubscribe()
44
51
  }
45
- }, [instance, disabled, error, projectIds])
52
+ }, [instance, isInactive, projectIds])
46
53
 
47
54
  return error
48
55
  }
@@ -37,11 +37,13 @@ describe('useDispatchIntent', () => {
37
37
  })
38
38
 
39
39
  it('should throw error when neither action nor intentId is provided', () => {
40
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
40
41
  const {result} = renderHook(() => useDispatchIntent({documentHandle: mockDocumentHandle}))
41
42
 
42
43
  expect(() => result.current.dispatchIntent()).toThrow(
43
44
  'useDispatchIntent: Either `action` or `intentId` must be provided.',
44
45
  )
46
+ consoleErrorSpy.mockRestore()
45
47
  })
46
48
 
47
49
  it('should handle errors gracefully', () => {
@@ -165,11 +167,11 @@ describe('useDispatchIntent', () => {
165
167
  })
166
168
  })
167
169
 
168
- it('should send intent message with media library source', () => {
170
+ it('should send intent message with media library resource', () => {
169
171
  const mockMediaLibraryHandle = {
170
172
  documentId: 'test-asset-id',
171
173
  documentType: 'sanity.asset',
172
- sourceName: 'media-library',
174
+ resourceName: 'media-library',
173
175
  } as const
174
176
 
175
177
  const {result} = renderHook(() =>
@@ -194,11 +196,11 @@ describe('useDispatchIntent', () => {
194
196
  })
195
197
  })
196
198
 
197
- it('should send intent message with canvas source', () => {
199
+ it('should send intent message with canvas resource', () => {
198
200
  const mockCanvasHandle = {
199
201
  documentId: 'test-canvas-document-id',
200
202
  documentType: 'sanity.canvas.document',
201
- sourceName: 'canvas',
203
+ resourceName: 'canvas',
202
204
  } as const
203
205
 
204
206
  const {result} = renderHook(() =>
@@ -224,7 +226,7 @@ describe('useDispatchIntent', () => {
224
226
  })
225
227
 
226
228
  describe('error handling', () => {
227
- it('should throw error when neither source nor projectId/dataset is provided', () => {
229
+ it('should throw error when neither resource nor projectId/dataset is provided', () => {
228
230
  const invalidHandle = {
229
231
  documentId: 'test-document-id',
230
232
  documentType: 'test-document-type',
@@ -238,7 +240,7 @@ describe('useDispatchIntent', () => {
238
240
  )
239
241
 
240
242
  expect(() => result.current.dispatchIntent()).toThrow(
241
- 'useDispatchIntent: Unable to determine resource. Either `source`, `sourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
243
+ 'useDispatchIntent: Unable to determine resource. Either `resource`, `resourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
242
244
  )
243
245
  })
244
246
  })
@@ -3,7 +3,7 @@ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
3
3
  import {useCallback} from 'react'
4
4
 
5
5
  import {useWindowConnection} from '../comlink/useWindowConnection'
6
- import {type WithSourceNameSupport} from '../helpers/useNormalizedSourceOptions'
6
+ import {type WithResourceNameSupport} from '../helpers/useNormalizedResourceOptions'
7
7
  import {useResourceIdFromDocumentHandle} from './utils/useResourceIdFromDocumentHandle'
8
8
 
9
9
  /**
@@ -42,7 +42,7 @@ interface DispatchIntent {
42
42
  interface UseDispatchIntentParams {
43
43
  action?: 'edit'
44
44
  intentId?: string
45
- documentHandle: WithSourceNameSupport<DocumentHandle>
45
+ documentHandle: WithResourceNameSupport<DocumentHandle>
46
46
  parameters?: Record<string, unknown>
47
47
  }
48
48
 
@@ -56,8 +56,8 @@ interface UseDispatchIntentParams {
56
56
  * - `action` - Action to perform (currently only 'edit' is supported). Will prompt a picker if multiple handlers are available.
57
57
  * - `intentId` - Specific ID of the intent to dispatch. Either `action` or `intentId` is required.
58
58
  * - `documentHandle` - The document handle containing document ID, type, and either:
59
- * - `projectId` and `dataset` for traditional dataset sources, like `{documentId: '123', documentType: 'book', projectId: 'abc123', dataset: 'production'}`
60
- * - `source` for media library, canvas, or dataset sources, like `{documentId: '123', documentType: 'sanity.asset', source: mediaLibrarySource('ml123')}` or `{documentId: '123', documentType: 'sanity.canvas.document', source: canvasSource('canvas123')}`
59
+ * - `projectId` and `dataset` for traditional dataset resources, like `{documentId: '123', documentType: 'book', projectId: 'abc123', dataset: 'production'}`
60
+ * - `resource` for media library, canvas, or dataset resources, like `{documentId: '123', documentType: 'sanity.asset', resource: mediaLibrarySource('ml123')}` or `{documentId: '123', documentType: 'sanity.canvas.document', resource: canvasSource('canvas123')}`
61
61
  * - `paremeters` - Optional parameters to include in the dispatch; will be passed to the resolved intent handler
62
62
  * @returns An object containing:
63
63
  * - `dispatchIntent` - Function to dispatch the intent message
@@ -119,10 +119,10 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
119
119
  )
120
120
  }
121
121
 
122
- // Validate that we have a resource ID (which is computed from source/sourceName or projectId+dataset)
122
+ // Validate that we have a resource ID (which is computed from resource/resourceName or projectId+dataset)
123
123
  if (!resource.id) {
124
124
  throw new Error(
125
- 'useDispatchIntent: Unable to determine resource. Either `source`, `sourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
125
+ 'useDispatchIntent: Unable to determine resource. Either `resource`, `resourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
126
126
  )
127
127
  }
128
128