@sanity/sdk-react 0.0.0-alpha.8 → 0.0.0-rc.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 (88) hide show
  1. package/README.md +33 -126
  2. package/dist/index.d.ts +4641 -2
  3. package/dist/index.js +960 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +17 -40
  6. package/src/_exports/index.ts +58 -10
  7. package/src/components/Login/LoginLinks.test.tsx +90 -0
  8. package/src/components/Login/LoginLinks.tsx +58 -0
  9. package/src/components/SDKProvider.test.tsx +79 -0
  10. package/src/components/SDKProvider.tsx +42 -0
  11. package/src/components/SanityApp.test.tsx +104 -2
  12. package/src/components/SanityApp.tsx +54 -17
  13. package/src/components/auth/AuthBoundary.test.tsx +2 -2
  14. package/src/components/auth/AuthBoundary.tsx +13 -3
  15. package/src/components/auth/Login.test.tsx +1 -1
  16. package/src/components/auth/Login.tsx +11 -26
  17. package/src/components/auth/LoginCallback.tsx +4 -7
  18. package/src/components/auth/LoginError.tsx +12 -8
  19. package/src/components/auth/LoginFooter.tsx +13 -20
  20. package/src/components/auth/LoginLayout.tsx +8 -9
  21. package/src/components/auth/authTestHelpers.tsx +1 -8
  22. package/src/components/utils.ts +22 -0
  23. package/src/context/SanityInstanceContext.ts +4 -0
  24. package/src/context/SanityProvider.test.tsx +1 -1
  25. package/src/context/SanityProvider.tsx +10 -8
  26. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  27. package/src/hooks/auth/useAuthState.tsx +0 -2
  28. package/src/hooks/auth/useCurrentUser.tsx +26 -20
  29. package/src/hooks/client/useClient.ts +8 -30
  30. package/src/hooks/comlink/useFrameConnection.test.tsx +45 -10
  31. package/src/hooks/comlink/useFrameConnection.ts +24 -5
  32. package/src/hooks/comlink/useManageFavorite.test.ts +106 -0
  33. package/src/hooks/comlink/useManageFavorite.ts +98 -0
  34. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +77 -0
  35. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +75 -0
  36. package/src/hooks/comlink/useWindowConnection.test.ts +43 -12
  37. package/src/hooks/comlink/useWindowConnection.ts +13 -1
  38. package/src/hooks/context/useSanityInstance.test.tsx +1 -1
  39. package/src/hooks/context/useSanityInstance.ts +21 -5
  40. package/src/hooks/datasets/useDatasets.ts +37 -0
  41. package/src/hooks/document/useApplyActions.test.ts +25 -0
  42. package/src/hooks/document/useApplyActions.ts +74 -0
  43. package/src/hooks/document/useDocument.test.ts +81 -0
  44. package/src/hooks/document/useDocument.ts +107 -0
  45. package/src/hooks/document/useDocumentEvent.test.ts +63 -0
  46. package/src/hooks/document/useDocumentEvent.ts +54 -0
  47. package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
  48. package/src/hooks/document/useDocumentSyncStatus.ts +30 -0
  49. package/src/hooks/document/useEditDocument.test.ts +179 -0
  50. package/src/hooks/document/useEditDocument.ts +195 -0
  51. package/src/hooks/document/usePermissions.ts +82 -0
  52. package/src/hooks/helpers/createCallbackHook.tsx +3 -2
  53. package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
  54. package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
  55. package/src/hooks/infiniteList/useInfiniteList.test.tsx +152 -0
  56. package/src/hooks/infiniteList/useInfiniteList.ts +174 -0
  57. package/src/hooks/paginatedList/usePaginatedList.test.tsx +259 -0
  58. package/src/hooks/paginatedList/usePaginatedList.ts +290 -0
  59. package/src/hooks/preview/usePreview.tsx +7 -4
  60. package/src/hooks/projection/useProjection.test.tsx +218 -0
  61. package/src/hooks/projection/useProjection.ts +135 -0
  62. package/src/hooks/projects/useProject.ts +45 -0
  63. package/src/hooks/projects/useProjects.ts +41 -0
  64. package/src/hooks/query/useQuery.test.tsx +188 -0
  65. package/src/hooks/query/useQuery.ts +103 -0
  66. package/src/hooks/users/useUsers.test.ts +163 -0
  67. package/src/hooks/users/useUsers.ts +107 -0
  68. package/src/utils/getEnv.ts +21 -0
  69. package/src/version.ts +8 -0
  70. package/dist/_chunks-es/context.js +0 -8
  71. package/dist/_chunks-es/context.js.map +0 -1
  72. package/dist/_chunks-es/useLogOut.js +0 -44
  73. package/dist/_chunks-es/useLogOut.js.map +0 -1
  74. package/dist/components.d.ts +0 -111
  75. package/dist/components.js +0 -153
  76. package/dist/components.js.map +0 -1
  77. package/dist/context.d.ts +0 -45
  78. package/dist/context.js +0 -5
  79. package/dist/context.js.map +0 -1
  80. package/dist/hooks.d.ts +0 -3485
  81. package/dist/hooks.js +0 -167
  82. package/dist/hooks.js.map +0 -1
  83. package/src/_exports/components.ts +0 -2
  84. package/src/_exports/context.ts +0 -2
  85. package/src/_exports/hooks.ts +0 -27
  86. package/src/hooks/client/useClient.test.tsx +0 -130
  87. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  88. package/src/hooks/documentCollection/useDocuments.ts +0 -135
@@ -0,0 +1,98 @@
1
+ import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
2
+ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
3
+ import {useCallback, useState} from 'react'
4
+
5
+ import {useWindowConnection} from './useWindowConnection'
6
+
7
+ // should we import this whole type from the message protocol?
8
+
9
+ interface ManageFavorite {
10
+ favorite: () => void
11
+ unfavorite: () => void
12
+ isFavorited: boolean
13
+ isConnected: boolean
14
+ }
15
+
16
+ /**
17
+ * @beta
18
+ *
19
+ * ## useManageFavorite
20
+ * This hook provides functionality to add and remove documents from favorites,
21
+ * and tracks the current favorite status of the document.
22
+ * @category Core UI Communication
23
+ * @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
24
+ * @returns An object containing:
25
+ * - `favorite` - Function to add document to favorites
26
+ * - `unfavorite` - Function to remove document from favorites
27
+ * - `isFavorited` - Boolean indicating if document is currently favorited
28
+ * - `isConnected` - Boolean indicating if connection to Core UI is established
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function MyDocumentAction(props: DocumentActionProps) {
33
+ * const {_id, _type} = props
34
+ * const {favorite, unfavorite, isFavorited, isConnected} = useManageFavorite({
35
+ * _id,
36
+ * _type
37
+ * })
38
+ *
39
+ * return (
40
+ * <Button
41
+ * disabled={!isConnected}
42
+ * onClick={() => isFavorited ? unfavorite() : favorite()}
43
+ * text={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
44
+ * />
45
+ * )
46
+ * }
47
+ * ```
48
+ */
49
+ export function useManageFavorite({_id, _type}: DocumentHandle): ManageFavorite {
50
+ const [isFavorited, setIsFavorited] = useState(false) // should load this from a comlink fetch
51
+ const {sendMessage, status} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
52
+ name: SDK_NODE_NAME,
53
+ connectTo: SDK_CHANNEL_NAME,
54
+ })
55
+
56
+ const handleFavoriteAction = useCallback(
57
+ (action: 'added' | 'removed', setFavoriteState: boolean) => {
58
+ if (!_id || !_type) return
59
+
60
+ try {
61
+ const message: Events.FavoriteMessage = {
62
+ type: 'core/v1/events/favorite',
63
+ data: {
64
+ eventType: action,
65
+ documentId: _id,
66
+ documentType: _type,
67
+ },
68
+ }
69
+
70
+ sendMessage(message.type, message.data)
71
+ setIsFavorited(setFavoriteState)
72
+ } catch (err) {
73
+ const error = err instanceof Error ? err : new Error('Failed to update favorite status')
74
+ // eslint-disable-next-line no-console
75
+ console.error(
76
+ `Failed to ${action === 'added' ? 'favorite' : 'unfavorite'} document:`,
77
+ error,
78
+ )
79
+ throw error
80
+ }
81
+ },
82
+ [_id, _type, sendMessage],
83
+ )
84
+
85
+ const favorite = useCallback(() => handleFavoriteAction('added', true), [handleFavoriteAction])
86
+
87
+ const unfavorite = useCallback(
88
+ () => handleFavoriteAction('removed', false),
89
+ [handleFavoriteAction],
90
+ )
91
+
92
+ return {
93
+ favorite,
94
+ unfavorite,
95
+ isFavorited,
96
+ isConnected: status === 'connected',
97
+ }
98
+ }
@@ -0,0 +1,77 @@
1
+ import {type Message, type Node, type Status} from '@sanity/comlink'
2
+ import {getOrCreateNode} from '@sanity/sdk'
3
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {act, renderHook} from '../../../test/test-utils'
6
+ import {useRecordDocumentHistoryEvent} from './useRecordDocumentHistoryEvent'
7
+
8
+ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
9
+ const actual = await importOriginal()
10
+ return {
11
+ ...actual,
12
+ getOrCreateNode: vi.fn(),
13
+ releaseNode: vi.fn(),
14
+ }
15
+ })
16
+
17
+ describe('useRecordDocumentHistoryEvent', () => {
18
+ let node: Node<Message, Message>
19
+ let statusCallback: ((status: Status) => void) | null = null
20
+
21
+ const mockDocumentHandle = {
22
+ _id: 'mock-id',
23
+ _type: 'mock-type',
24
+ }
25
+
26
+ function createMockNode() {
27
+ return {
28
+ on: vi.fn(() => () => {}),
29
+ post: vi.fn(),
30
+ stop: vi.fn(),
31
+ onStatus: vi.fn((callback) => {
32
+ statusCallback = callback
33
+ return () => {}
34
+ }),
35
+ } as unknown as Node<Message, Message>
36
+ }
37
+
38
+ beforeEach(() => {
39
+ statusCallback = null
40
+ node = createMockNode()
41
+ vi.mocked(getOrCreateNode).mockReturnValue(node)
42
+ })
43
+
44
+ it('should initialize with correct connection status', () => {
45
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
46
+
47
+ expect(result.current.isConnected).toBe(false)
48
+
49
+ act(() => {
50
+ statusCallback?.('connected')
51
+ })
52
+
53
+ expect(result.current.isConnected).toBe(true)
54
+ })
55
+
56
+ it('should send correct message when recording events', () => {
57
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
58
+
59
+ result.current.recordEvent('viewed')
60
+
61
+ expect(node.post).toHaveBeenCalledWith('core/v1/events/history', {
62
+ eventType: 'viewed',
63
+ documentId: 'mock-id',
64
+ documentType: 'mock-type',
65
+ })
66
+ })
67
+
68
+ it('should handle errors when sending messages', () => {
69
+ vi.mocked(node.post).mockImplementation(() => {
70
+ throw new Error('Failed to send message')
71
+ })
72
+
73
+ const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
74
+
75
+ expect(() => result.current.recordEvent('viewed')).toThrow('Failed to send message')
76
+ })
77
+ })
@@ -0,0 +1,75 @@
1
+ import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
2
+ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
3
+ import {useCallback} from 'react'
4
+
5
+ import {useWindowConnection} from './useWindowConnection'
6
+
7
+ interface DocumentInteractionHistory {
8
+ recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
9
+ isConnected: boolean
10
+ }
11
+
12
+ /**
13
+ * @public
14
+ * Hook for managing document interaction history in Sanity Studio.
15
+ * This hook provides functionality to record document interactions.
16
+ * @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
17
+ * @returns An object containing:
18
+ * - `recordEvent` - Function to record document interactions
19
+ * - `isConnected` - Boolean indicating if connection to Studio is established
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * function MyDocumentAction(props: DocumentActionProps) {
24
+ * const {_id, _type} = props
25
+ * const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
26
+ * _id,
27
+ * _type
28
+ * })
29
+ *
30
+ * return (
31
+ * <Button
32
+ * disabled={!isConnected}
33
+ * onClick={() => recordEvent('viewed')}
34
+ * text={'Viewed'}
35
+ * />
36
+ * )
37
+ * }
38
+ * ```
39
+ */
40
+ export function useRecordDocumentHistoryEvent({
41
+ _id,
42
+ _type,
43
+ }: DocumentHandle): DocumentInteractionHistory {
44
+ const {sendMessage, status} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
45
+ name: SDK_NODE_NAME,
46
+ connectTo: SDK_CHANNEL_NAME,
47
+ })
48
+
49
+ const recordEvent = useCallback(
50
+ (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
51
+ try {
52
+ const message: Events.HistoryMessage = {
53
+ type: 'core/v1/events/history',
54
+ data: {
55
+ eventType,
56
+ documentId: _id,
57
+ documentType: _type,
58
+ },
59
+ }
60
+
61
+ sendMessage(message.type, message.data)
62
+ } catch (error) {
63
+ // eslint-disable-next-line no-console
64
+ console.error('Failed to record history event:', error)
65
+ throw error
66
+ }
67
+ },
68
+ [_id, _type, sendMessage],
69
+ )
70
+
71
+ return {
72
+ recordEvent,
73
+ isConnected: status === 'connected',
74
+ }
75
+ }
@@ -1,8 +1,8 @@
1
- import {type Message, type Node} from '@sanity/comlink'
1
+ import {type Message, type Node, type Status} from '@sanity/comlink'
2
2
  import {getOrCreateNode, releaseNode} from '@sanity/sdk'
3
3
  import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
 
5
- import {renderHook} from '../../../test/test-utils'
5
+ import {act, renderHook} from '../../../test/test-utils'
6
6
  import {useWindowConnection} from './useWindowConnection'
7
7
 
8
8
  vi.mock(import('@sanity/sdk'), async (importOriginal) => {
@@ -26,24 +26,55 @@ interface AnotherMessage {
26
26
 
27
27
  type TestMessages = TestMessage | AnotherMessage
28
28
 
29
- function createMockNode() {
30
- return {
31
- // return unsubscribe function
32
- on: vi.fn(() => () => {}),
33
- post: vi.fn(),
34
- stop: vi.fn(),
35
- } as unknown as Node<Message, Message>
36
- }
37
-
38
29
  describe('useWindowConnection', () => {
39
30
  let node: Node<Message, Message>
31
+ let statusCallback: ((status: Status) => void) | null = null
32
+
33
+ function createMockNode() {
34
+ return {
35
+ // return unsubscribe function
36
+ on: vi.fn(() => () => {}),
37
+ post: vi.fn(),
38
+ stop: vi.fn(),
39
+ onStatus: vi.fn((callback) => {
40
+ statusCallback = callback
41
+ return () => {}
42
+ }),
43
+ } as unknown as Node<Message, Message>
44
+ }
40
45
 
41
46
  beforeEach(() => {
47
+ statusCallback = null
42
48
  node = createMockNode()
43
-
44
49
  vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
45
50
  })
46
51
 
52
+ it('should initialize with idle status', () => {
53
+ const {result} = renderHook(() =>
54
+ useWindowConnection<TestMessages, TestMessages>({
55
+ name: 'test',
56
+ connectTo: 'window',
57
+ }),
58
+ )
59
+
60
+ expect(result.current.status).toBe('idle')
61
+ })
62
+
63
+ it('should update status to connected when node connects', async () => {
64
+ const {result} = renderHook(() =>
65
+ useWindowConnection<TestMessages, TestMessages>({
66
+ name: 'test',
67
+ connectTo: 'window',
68
+ }),
69
+ )
70
+
71
+ expect(result.current.status).toBe('idle')
72
+ act(() => {
73
+ statusCallback?.('connected')
74
+ })
75
+ expect(result.current.status).toBe('connected')
76
+ })
77
+
47
78
  it('should register message handlers', () => {
48
79
  const mockHandler = vi.fn()
49
80
  const mockData = {someData: 'test'}
@@ -1,5 +1,6 @@
1
+ import {type Status} from '@sanity/comlink'
1
2
  import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
2
- import {useCallback, useEffect, useMemo} from 'react'
3
+ import {useCallback, useEffect, useMemo, useState} from 'react'
3
4
 
4
5
  import {useSanityInstance} from '../context/useSanityInstance'
5
6
 
@@ -27,6 +28,7 @@ export interface WindowConnection<TMessage extends WindowMessage> {
27
28
  type: TType,
28
29
  data?: Extract<TMessage, {type: TType}>['data'],
29
30
  ) => void
31
+ status: Status
30
32
  }
31
33
 
32
34
  /**
@@ -38,12 +40,21 @@ export function useWindowConnection<
38
40
  >(options: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {
39
41
  const {name, onMessage, connectTo} = options
40
42
  const instance = useSanityInstance()
43
+ const [status, setStatus] = useState<Status>('idle')
41
44
 
42
45
  const node = useMemo(
43
46
  () => getOrCreateNode(instance, {name, connectTo}),
44
47
  [instance, name, connectTo],
45
48
  )
46
49
 
50
+ useEffect(() => {
51
+ const unsubscribe = node.onStatus((newStatus) => {
52
+ setStatus(newStatus)
53
+ })
54
+
55
+ return unsubscribe
56
+ }, [node, instance, name])
57
+
47
58
  useEffect(() => {
48
59
  if (!onMessage) return
49
60
 
@@ -78,5 +89,6 @@ export function useWindowConnection<
78
89
 
79
90
  return {
80
91
  sendMessage,
92
+ status,
81
93
  }
82
94
  }
@@ -11,7 +11,7 @@ describe('useSanityInstance', () => {
11
11
 
12
12
  it('returns sanity instance when used within provider', () => {
13
13
  const wrapper = ({children}: {children: React.ReactNode}) => (
14
- <SanityProvider sanityInstance={sanityInstance}>{children}</SanityProvider>
14
+ <SanityProvider sanityInstances={[sanityInstance]}>{children}</SanityProvider>
15
15
  )
16
16
 
17
17
  const {result} = renderHook(() => useSanityInstance(), {wrapper})
@@ -1,23 +1,39 @@
1
1
  import {type SanityInstance} from '@sanity/sdk'
2
2
  import {useContext} from 'react'
3
3
 
4
- import {SanityInstanceContext} from '../../context/SanityProvider'
4
+ import {SanityInstanceContext} from '../../context/SanityInstanceContext'
5
5
 
6
6
  /**
7
7
  * `useSanityInstance` returns the current Sanity instance from the application context.
8
8
  * This must be called from within a `SanityProvider` component.
9
- * @public
9
+ * @internal
10
+ *
11
+ * @param resourceId - The resourceId of the Sanity instance to return (optional)
10
12
  * @returns The current Sanity instance
11
13
  * @example
12
14
  * ```tsx
13
- * const instance = useSanityInstance()
15
+ * const instance = useSanityInstance('abc123.production')
14
16
  * ```
15
17
  */
16
- export const useSanityInstance = (): SanityInstance => {
18
+ export const useSanityInstance = (resourceId?: string): SanityInstance => {
17
19
  const sanityInstance = useContext(SanityInstanceContext)
18
20
  if (!sanityInstance) {
19
21
  throw new Error('useSanityInstance must be called from within the SanityProvider')
20
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
+ }
21
29
 
22
- return sanityInstance
30
+ if (!resourceId) {
31
+ throw new Error('resourceId is required when there are multiple Sanity instances')
32
+ }
33
+
34
+ const instance = sanityInstance.find((inst) => inst.identity.resourceId === resourceId)
35
+ if (!instance) {
36
+ throw new Error(`Sanity instance with resourceId ${resourceId} not found`)
37
+ }
38
+ return instance
23
39
  }
@@ -0,0 +1,37 @@
1
+ import {type DatasetsResponse} from '@sanity/client'
2
+ import {getDatasetsState, resolveDatasets, type SanityInstance, type StateSource} from '@sanity/sdk'
3
+
4
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
+
6
+ type UseDatasets = {
7
+ /**
8
+ *
9
+ * Returns metadata for each dataset in your organization.
10
+ *
11
+ * @category Datasets
12
+ * @returns The metadata for your organization's datasets
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const datasets = useDatasets()
17
+ *
18
+ * return (
19
+ * <select>
20
+ * {datasets.map((dataset) => (
21
+ * <option key={dataset.name}>{dataset.name}</option>
22
+ * ))}
23
+ * </select>
24
+ * )
25
+ * ```
26
+ *
27
+ */
28
+ (): DatasetsResponse
29
+ }
30
+
31
+ /** @public */
32
+ export const useDatasets: UseDatasets = createStateSourceHook({
33
+ // remove `undefined` since we're suspending when that is the case
34
+ getState: getDatasetsState as (instance: SanityInstance) => StateSource<DatasetsResponse>,
35
+ shouldSuspend: (instance) => getDatasetsState(instance).getCurrent() === undefined,
36
+ suspender: resolveDatasets,
37
+ })
@@ -0,0 +1,25 @@
1
+ import {applyActions, createDocument, type ResourceId} from '@sanity/sdk'
2
+ import {describe, it} from 'vitest'
3
+
4
+ import {createCallbackHook} from '../helpers/createCallbackHook'
5
+
6
+ vi.mock('../helpers/createCallbackHook', () => ({
7
+ createCallbackHook: vi.fn((cb) => () => cb),
8
+ }))
9
+ vi.mock('@sanity/sdk', async (importOriginal) => {
10
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
11
+ return {...original, applyActions: vi.fn()}
12
+ })
13
+
14
+ describe('useApplyActions', () => {
15
+ it('calls `createCallbackHook` with `applyActions`', async () => {
16
+ const {useApplyActions} = await import('./useApplyActions')
17
+ const resourceId: ResourceId = 'project1.dataset1'
18
+ expect(createCallbackHook).not.toHaveBeenCalled()
19
+
20
+ expect(applyActions).not.toHaveBeenCalled()
21
+ const apply = useApplyActions(resourceId)
22
+ apply(createDocument({_type: 'author'}))
23
+ expect(applyActions).toHaveBeenCalledWith(createDocument({_type: 'author'}))
24
+ })
25
+ })
@@ -0,0 +1,74 @@
1
+ import {
2
+ type ActionsResult,
3
+ applyActions,
4
+ type ApplyActionsOptions,
5
+ type DocumentAction,
6
+ type ResourceId,
7
+ } from '@sanity/sdk'
8
+ import {type SanityDocument} from '@sanity/types'
9
+
10
+ import {createCallbackHook} from '../helpers/createCallbackHook'
11
+
12
+ /**
13
+ *
14
+ * @beta
15
+ *
16
+ * Provides a callback for applying one or more actions to a document.
17
+ *
18
+ * @category Documents
19
+ * @param resourceId - The resource ID of the document to apply actions to. If not provided, the document will use the default resource.
20
+ * @returns A function that takes one more more {@link DocumentAction}s and returns a promise that resolves to an {@link ActionsResult}.
21
+ * @example Publish or unpublish a document
22
+ * ```
23
+ * import { publishDocument, unpublishDocument } from '@sanity/sdk'
24
+ * import { useApplyActions } from '@sanity/sdk-react'
25
+ *
26
+ * const apply = useApplyActions()
27
+ * const myDocument = { _id: 'my-document-id', _type: 'my-document-type' }
28
+ *
29
+ * return (
30
+ * <button onClick={() => apply(publishDocument(myDocument))}>Publish</button>
31
+ * <button onClick={() => apply(unpublishDocument(myDocument))}>Unpublish</button>
32
+ * )
33
+ * ```
34
+ *
35
+ * @example Create and publish a new document
36
+ * ```
37
+ * import { createDocument, publishDocument } from '@sanity/sdk'
38
+ * import { useApplyActions } from '@sanity/sdk-react'
39
+ *
40
+ * const apply = useApplyActions()
41
+ *
42
+ * const handleCreateAndPublish = () => {
43
+ * const handle = { _id: window.crypto.randomUUID(), _type: 'my-document-type' }
44
+ * apply([
45
+ * createDocument(handle),
46
+ * publishDocument(handle),
47
+ * ])
48
+ * }
49
+ *
50
+ * return (
51
+ * <button onClick={handleCreateAndPublish}>
52
+ * I’m feeling lucky
53
+ * </button>
54
+ * )
55
+ * ```
56
+ */
57
+ export function useApplyActions(
58
+ resourceId?: ResourceId,
59
+ ): <TDocument extends SanityDocument>(
60
+ action: DocumentAction<TDocument> | DocumentAction<TDocument>[],
61
+ options?: ApplyActionsOptions,
62
+ ) => Promise<ActionsResult<TDocument>>
63
+
64
+ /** @beta */
65
+ export function useApplyActions(
66
+ resourceId?: ResourceId,
67
+ ): (
68
+ action: DocumentAction | DocumentAction[],
69
+ options?: ApplyActionsOptions,
70
+ ) => Promise<ActionsResult> {
71
+ return _useApplyActions(resourceId)()
72
+ }
73
+
74
+ const _useApplyActions = (resourceId?: ResourceId) => createCallbackHook(applyActions, resourceId)
@@ -0,0 +1,81 @@
1
+ // tests/useDocument.test.ts
2
+ import {
3
+ createSanityInstance,
4
+ getDocumentState,
5
+ resolveDocument,
6
+ type StateSource,
7
+ } from '@sanity/sdk'
8
+ import {type SanityDocument} from '@sanity/types'
9
+ import {renderHook} from '@testing-library/react'
10
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
11
+
12
+ import {useSanityInstance} from '../context/useSanityInstance'
13
+ import {useDocument} from './useDocument'
14
+
15
+ vi.mock('@sanity/sdk', async (importOriginal) => {
16
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
17
+ return {...original, getDocumentState: vi.fn(), resolveDocument: vi.fn()}
18
+ })
19
+
20
+ vi.mock('../context/useSanityInstance', () => ({
21
+ useSanityInstance: vi.fn(),
22
+ }))
23
+
24
+ // Create a fake instance to be returned by useSanityInstance.
25
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
26
+ const doc: SanityDocument = {
27
+ _id: 'doc1',
28
+ foo: 'bar',
29
+ _type: 'book',
30
+ _rev: 'tx0',
31
+ _createdAt: '2025-02-06T00:11:00.000Z',
32
+ _updatedAt: '2025-02-06T00:11:00.000Z',
33
+ }
34
+
35
+ describe('useDocument hook', () => {
36
+ beforeEach(() => {
37
+ vi.resetAllMocks()
38
+ vi.mocked(useSanityInstance).mockReturnValue(instance)
39
+ })
40
+
41
+ it('returns the current document when ready (without a path)', () => {
42
+ const getCurrent = vi.fn().mockReturnValue(doc)
43
+ const subscribe = vi.fn().mockReturnValue(vi.fn())
44
+ vi.mocked(getDocumentState).mockReturnValue({
45
+ getCurrent,
46
+ subscribe,
47
+ } as unknown as StateSource<unknown>)
48
+
49
+ const {result} = renderHook(() => useDocument({_id: 'doc1', _type: 'book'}))
50
+
51
+ expect(result.current).toEqual(doc)
52
+ expect(getCurrent).toHaveBeenCalled()
53
+ expect(subscribe).toHaveBeenCalled()
54
+ })
55
+
56
+ it('throws a promise (suspends) when the document is not ready', () => {
57
+ const getCurrent = vi.fn().mockReturnValue(undefined)
58
+ const subscribe = vi.fn().mockReturnValue(vi.fn())
59
+ vi.mocked(getDocumentState).mockReturnValue({
60
+ getCurrent,
61
+ subscribe,
62
+ } as unknown as StateSource<unknown>)
63
+
64
+ const resolveDocPromise = Promise.resolve(doc)
65
+
66
+ // Also, simulate resolveDocument to return a known promise.
67
+ vi.mocked(resolveDocument).mockReturnValue(resolveDocPromise)
68
+
69
+ // Render the hook and capture the thrown promise.
70
+ const {result} = renderHook(() => {
71
+ try {
72
+ return useDocument({_id: 'doc1', _type: 'book'})
73
+ } catch (e) {
74
+ return e
75
+ }
76
+ })
77
+
78
+ // When the document is not ready, the hook throws the promise from resolveDocument.
79
+ expect(result.current).toBe(resolveDocPromise)
80
+ })
81
+ })