@sanity/sdk-react 0.0.0-alpha.9 → 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 -41
  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 +5 -4
  42. package/src/hooks/document/useApplyActions.ts +55 -5
  43. package/src/hooks/document/useDocument.test.ts +2 -2
  44. package/src/hooks/document/useDocument.ts +90 -21
  45. package/src/hooks/document/useDocumentEvent.test.ts +13 -3
  46. package/src/hooks/document/useDocumentEvent.ts +36 -4
  47. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -1
  48. package/src/hooks/document/useDocumentSyncStatus.ts +26 -2
  49. package/src/hooks/document/useEditDocument.test.ts +55 -10
  50. package/src/hooks/document/useEditDocument.ts +159 -31
  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 -45
  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 -3532
  81. package/dist/hooks.js +0 -218
  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 -32
  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
@@ -1,38 +1,107 @@
1
1
  import {
2
2
  type DocumentHandle,
3
3
  getDocumentState,
4
+ getResourceId,
4
5
  type JsonMatch,
5
6
  type JsonMatchPath,
6
7
  resolveDocument,
7
8
  } from '@sanity/sdk'
8
9
  import {type SanityDocument} from '@sanity/types'
9
- import {useCallback, useMemo, useSyncExternalStore} from 'react'
10
10
 
11
- import {useSanityInstance} from '../context/useSanityInstance'
11
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
12
 
13
- /** @beta */
13
+ /**
14
+ * @beta
15
+ *
16
+ * ## useDocument(doc, path)
17
+ * Read and subscribe to nested values in a document
18
+ * @category Documents
19
+ * @param doc - The document to read state from. If you pass a `DocumentHandle` with a `resourceId` in the DocumentResourceId format (`document:projectId.dataset:documentId`)
20
+ * the document will be read from the specified Sanity project and dataset that is included in the handle. If no `resourceId` is provided, the default project and dataset from your `SanityApp` configuration will be used.
21
+ * @param path - The path to the nested value to read from
22
+ * @returns The value at the specified path
23
+ * @example
24
+ * ```tsx
25
+ * import {type DocumentHandle, useDocument} from '@sanity/sdk-react'
26
+ *
27
+ * function OrderLink({documentHandle}: {documentHandle: DocumentHandle}) {
28
+ * const title = useDocument(documentHandle, 'title')
29
+ * const id = useDocument(documentHandle, '_id')
30
+ *
31
+ * return (
32
+ * <a href=`/order/${id}`>Order {title} today!</a>
33
+ * )
34
+ * }
35
+ * ```
36
+ *
37
+ */
14
38
  export function useDocument<
15
39
  TDocument extends SanityDocument,
16
40
  TPath extends JsonMatchPath<TDocument>,
17
- >(doc: string | DocumentHandle<TDocument>, path: TPath): JsonMatch<TDocument, TPath> | undefined
18
- /** @beta */
41
+ >(doc: DocumentHandle<TDocument>, path: TPath): JsonMatch<TDocument, TPath> | undefined
42
+
43
+ /**
44
+ * @beta
45
+ * ## useDocument(doc)
46
+ * Read and subscribe to an entire document
47
+ * @param doc - The document to read state from
48
+ * @returns The document state as an object
49
+ * @example
50
+ * ```tsx
51
+ * import {type SanityDocument, type DocumentHandle, useDocument} from '@sanity/sdk-react'
52
+ *
53
+ * interface Book extends SanityDocument {
54
+ * title: string
55
+ * author: string
56
+ * summary: string
57
+ * }
58
+ *
59
+ * function DocumentView({documentHandle}: {documentHandle: DocumentHandle}) {
60
+ * const book = useDocument<Book>(documentHandle)
61
+ *
62
+ * return (
63
+ * <article>
64
+ * <h1>{book?.title}</h1>
65
+ * <address>By {book?.author}</address>
66
+ *
67
+ * <h2>Summary</h2>
68
+ * {book?.summary}
69
+ *
70
+ * <h2>Order</h2>
71
+ * <a href=`/order/${book._id}`>Order {book?.title} today!</a>
72
+ * </article>
73
+ * )
74
+ * }
75
+ * ```
76
+ *
77
+ */
19
78
  export function useDocument<TDocument extends SanityDocument>(
20
- doc: string | DocumentHandle<TDocument>,
79
+ doc: DocumentHandle<TDocument>,
21
80
  ): TDocument | null
22
- /** @beta */
23
- export function useDocument(doc: string | DocumentHandle, path?: string): unknown {
24
- const documentId = typeof doc === 'string' ? doc : doc._id
25
- const instance = useSanityInstance()
26
- const isDocumentReady = useCallback(
27
- () => getDocumentState(instance, documentId).getCurrent() !== undefined,
28
- [instance, documentId],
29
- )
30
- if (!isDocumentReady()) throw resolveDocument(instance, documentId)
31
-
32
- const {subscribe, getCurrent} = useMemo(
33
- () => getDocumentState(instance, documentId, path),
34
- [documentId, instance, path],
35
- )
36
81
 
37
- return useSyncExternalStore(subscribe, getCurrent)
82
+ /**
83
+ * @beta
84
+ * Reads and subscribes to a document’s realtime state, incorporating both local and remote changes.
85
+ * When called with a `path` argument, the hook will return the nested value’s state.
86
+ * When called without a `path` argument, the entire document’s state will be returned.
87
+ *
88
+ * @remarks
89
+ * `useDocument` is designed to be used within a realtime context in which local updates to documents
90
+ * need to be displayed before they are persisted to the remote copy. This can be useful within a collaborative
91
+ * or realtime editing interface where local changes need to be reflected immediately.
92
+ *
93
+ * However, this hook can be too resource intensive for applications where static document values simply
94
+ * need to be displayed (or when changes to documents don’t need to be reflected immediately);
95
+ * consider using `usePreview` or `useQuery` for these use cases instead. These hooks leverage the Sanity
96
+ * Live Content API to provide a more efficient way to read and subscribe to document state.
97
+ */
98
+ export function useDocument(doc: DocumentHandle, path?: string): unknown {
99
+ return _useDocument(doc, path)
38
100
  }
101
+
102
+ const _useDocument = createStateSourceHook<[doc: DocumentHandle, path?: string], unknown>({
103
+ getState: getDocumentState,
104
+ shouldSuspend: (instance, doc) => getDocumentState(instance, doc._id).getCurrent() === undefined,
105
+ suspender: resolveDocument,
106
+ getResourceId: (doc) => getResourceId(doc.resourceId),
107
+ })
@@ -1,5 +1,10 @@
1
1
  // tests/useDocumentEvent.test.ts
2
- import {createSanityInstance, type DocumentEvent, subscribeDocumentEvents} from '@sanity/sdk'
2
+ import {
3
+ createSanityInstance,
4
+ type DocumentEvent,
5
+ type DocumentHandle,
6
+ subscribeDocumentEvents,
7
+ } from '@sanity/sdk'
3
8
  import {renderHook} from '@testing-library/react'
4
9
  import {beforeEach, describe, expect, it, vi} from 'vitest'
5
10
 
@@ -16,6 +21,11 @@ vi.mock('../context/useSanityInstance', () => ({
16
21
  }))
17
22
 
18
23
  const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
24
+ const docHandle: DocumentHandle = {
25
+ _id: 'doc1',
26
+ _type: 'book',
27
+ resourceId: 'document:p.d:doc1',
28
+ }
19
29
 
20
30
  describe('useDocumentEvent hook', () => {
21
31
  beforeEach(() => {
@@ -28,7 +38,7 @@ describe('useDocumentEvent hook', () => {
28
38
  const unsubscribe = vi.fn()
29
39
  vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
30
40
 
31
- renderHook(() => useDocumentEvent(handler))
41
+ renderHook(() => useDocumentEvent(handler, docHandle))
32
42
 
33
43
  expect(vi.mocked(subscribeDocumentEvents)).toHaveBeenCalledTimes(1)
34
44
  expect(vi.mocked(subscribeDocumentEvents).mock.calls[0][0]).toBe(instance)
@@ -46,7 +56,7 @@ describe('useDocumentEvent hook', () => {
46
56
  const unsubscribe = vi.fn()
47
57
  vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
48
58
 
49
- const {unmount} = renderHook(() => useDocumentEvent(handler))
59
+ const {unmount} = renderHook(() => useDocumentEvent(handler, docHandle))
50
60
  unmount()
51
61
  expect(unsubscribe).toHaveBeenCalledTimes(1)
52
62
  })
@@ -1,10 +1,42 @@
1
- import {type DocumentEvent, subscribeDocumentEvents} from '@sanity/sdk'
1
+ import {
2
+ type DocumentEvent,
3
+ type DocumentHandle,
4
+ getResourceId,
5
+ subscribeDocumentEvents,
6
+ } from '@sanity/sdk'
2
7
  import {useCallback, useEffect, useInsertionEffect, useRef} from 'react'
3
8
 
4
9
  import {useSanityInstance} from '../context/useSanityInstance'
5
10
 
6
- /** @beta */
7
- export function useDocumentEvent(handler: (documentEvent: DocumentEvent) => void): void {
11
+ /**
12
+ *
13
+ * @beta
14
+ *
15
+ * Subscribes an event handler to events in your application’s document store, such as document
16
+ * creation, deletion, and updates.
17
+ *
18
+ * @category Documents
19
+ * @param handler - The event handler to register.
20
+ * @param doc - The document to subscribe to events for. If you pass a `DocumentHandle` with a `resourceId` (in the format of `document:projectId.dataset:documentId`)
21
+ * the document will be read from the specified Sanity project and dataset that is included in the handle. If no `resourceId` is provided, the default project and dataset from your `SanityApp` configuration will be used.
22
+ * @example
23
+ * ```
24
+ * import {useDocumentEvent} from '@sanity/sdk-react'
25
+ * import {type DocumentEvent} from '@sanity/sdk'
26
+ *
27
+ * useDocumentEvent((event) => {
28
+ * if (event.type === DocumentEvent.DocumentDeletedEvent) {
29
+ * alert(`Document with ID ${event.documentId} deleted!`)
30
+ * } else {
31
+ * console.log(event)
32
+ * }
33
+ * })
34
+ * ```
35
+ */
36
+ export function useDocumentEvent(
37
+ handler: (documentEvent: DocumentEvent) => void,
38
+ doc: DocumentHandle,
39
+ ): void {
8
40
  const ref = useRef(handler)
9
41
 
10
42
  useInsertionEffect(() => {
@@ -15,7 +47,7 @@ export function useDocumentEvent(handler: (documentEvent: DocumentEvent) => void
15
47
  return ref.current(documentEvent)
16
48
  }, [])
17
49
 
18
- const instance = useSanityInstance()
50
+ const instance = useSanityInstance(getResourceId(doc.resourceId))
19
51
  useEffect(() => {
20
52
  return subscribeDocumentEvents(instance, stableHandler)
21
53
  }, [instance, stableHandler])
@@ -7,7 +7,7 @@ import {createStateSourceHook} from '../helpers/createStateSourceHook'
7
7
  vi.mock('../helpers/createStateSourceHook', () => ({createStateSourceHook: vi.fn(identity)}))
8
8
  vi.mock('@sanity/sdk', () => ({getDocumentSyncStatus: vi.fn()}))
9
9
 
10
- describe('useAuthToken', () => {
10
+ describe('useDocumentSyncStatus', () => {
11
11
  it('calls `createStateSourceHook` with `getTokenState`', async () => {
12
12
  const {useDocumentSyncStatus} = await import('./useDocumentSyncStatus')
13
13
  expect(createStateSourceHook).toHaveBeenCalledWith(getDocumentSyncStatus)
@@ -1,6 +1,30 @@
1
- import {getDocumentSyncStatus} from '@sanity/sdk'
1
+ import {type DocumentHandle, getDocumentSyncStatus} from '@sanity/sdk'
2
2
 
3
3
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
4
4
 
5
+ type UseDocumentSyncStatus = {
6
+ /**
7
+ * Exposes the document’s sync status between local and remote document states.
8
+ *
9
+ * @category Documents
10
+ * @param doc - The document handle to get sync status for. If you pass a `DocumentHandle` with a `resourceId` (in the format of `document:projectId.dataset:documentId`)
11
+ * the document will be read from the specified Sanity project and dataset that is included in the handle. If no `resourceId` is provided, the default project and dataset from your `SanityApp` configuration will be used.
12
+ * @returns `true` if local changes are synced with remote, `false` if the changes are not synced, and `undefined` if the document is not found
13
+ * @example Disable a Save button when there are no changes to sync
14
+ * ```
15
+ * const myDocumentHandle = { _id: 'documentId', _type: 'documentType', resourceId: 'document:projectId:dataset:documentId' }
16
+ * const documentSynced = useDocumentSyncStatus(myDocumentHandle)
17
+ *
18
+ * return (
19
+ * <button disabled={documentSynced}>
20
+ * Save Changes
21
+ * </button>
22
+ * )
23
+ * ```
24
+ */
25
+ (doc: DocumentHandle): boolean | undefined
26
+ }
27
+
5
28
  /** @beta */
6
- export const useDocumentSyncStatus = createStateSourceHook(getDocumentSyncStatus)
29
+ export const useDocumentSyncStatus: UseDocumentSyncStatus =
30
+ createStateSourceHook(getDocumentSyncStatus)
@@ -1,6 +1,7 @@
1
1
  // tests/useEditDocument.test.ts
2
2
  import {
3
3
  createSanityInstance,
4
+ type DocumentHandle,
4
5
  editDocument,
5
6
  getDocumentState,
6
7
  resolveDocument,
@@ -35,13 +36,19 @@ vi.mock('./useApplyActions', () => ({
35
36
  // Create a fake instance to be returned by useSanityInstance.
36
37
  const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
37
38
 
38
- const doc: SanityDocument = {
39
+ const doc = {
39
40
  _id: 'doc1',
40
41
  foo: 'bar',
41
42
  _type: 'book',
42
43
  _rev: 'tx0',
43
44
  _createdAt: '2025-02-06T00:11:00.000Z',
44
45
  _updatedAt: '2025-02-06T00:11:00.000Z',
46
+ } satisfies SanityDocument
47
+
48
+ const docHandle: DocumentHandle<SanityDocument> = {
49
+ _id: 'doc1',
50
+ _type: 'book',
51
+ resourceId: 'document:p.d:doc1',
45
52
  }
46
53
 
47
54
  describe('useEditDocument hook', () => {
@@ -61,10 +68,10 @@ describe('useEditDocument hook', () => {
61
68
  const apply = vi.fn().mockResolvedValue({transactionId: 'tx1'})
62
69
  vi.mocked(useApplyActions).mockReturnValue(apply)
63
70
 
64
- const {result} = renderHook(() => useEditDocument('doc1', 'foo'))
71
+ const {result} = renderHook(() => useEditDocument(docHandle, 'foo'))
65
72
  const promise = result.current('newValue')
66
- expect(editDocument).toHaveBeenCalledWith('doc1', {set: {foo: 'newValue'}})
67
- expect(apply).toHaveBeenCalledWith(editDocument('doc1', {set: {foo: 'newValue'}}))
73
+ expect(editDocument).toHaveBeenCalledWith(docHandle, {set: {foo: 'newValue'}})
74
+ expect(apply).toHaveBeenCalledWith(editDocument(docHandle, {set: {foo: 'newValue'}}))
68
75
  const actionsResult = await promise
69
76
  expect(actionsResult).toEqual({transactionId: 'tx1'})
70
77
  })
@@ -82,13 +89,51 @@ describe('useEditDocument hook', () => {
82
89
  const apply = vi.fn().mockResolvedValue({transactionId: 'tx2'})
83
90
  vi.mocked(useApplyActions).mockReturnValue(apply)
84
91
 
85
- const {result} = renderHook(() => useEditDocument('doc1'))
86
- const promise = result.current({foo: 'baz', extra: 'old', _id: 'doc1'})
87
- expect(apply).toHaveBeenCalledWith([editDocument('doc1', {set: {foo: 'baz'}})])
92
+ const {result} = renderHook(() => useEditDocument(docHandle))
93
+ const promise = result.current({...doc, foo: 'baz', extra: 'old', _id: 'doc1'})
94
+ expect(apply).toHaveBeenCalledWith([editDocument(docHandle, {set: {foo: 'baz'}})])
88
95
  const actionsResult = await promise
89
96
  expect(actionsResult).toEqual({transactionId: 'tx2'})
90
97
  })
91
98
 
99
+ it('applies a single edit action using an updater function for the given path', async () => {
100
+ const getCurrent = vi.fn().mockReturnValue(doc.foo)
101
+ const subscribe = vi.fn().mockReturnValue(vi.fn())
102
+ vi.mocked(getDocumentState).mockReturnValue({
103
+ getCurrent,
104
+ subscribe,
105
+ } as unknown as StateSource<SanityDocument>)
106
+
107
+ const apply = vi.fn().mockResolvedValue({transactionId: 'tx3'})
108
+ vi.mocked(useApplyActions).mockReturnValue(apply)
109
+
110
+ const {result} = renderHook(() => useEditDocument(docHandle, 'foo'))
111
+ const promise = result.current((prev: unknown) => `${prev}Updated`) // 'bar' becomes 'barUpdated'
112
+ expect(editDocument).toHaveBeenCalledWith(docHandle, {set: {foo: 'barUpdated'}})
113
+ expect(apply).toHaveBeenCalledWith(editDocument(docHandle, {set: {foo: 'barUpdated'}}))
114
+ const actionsResult = await promise
115
+ expect(actionsResult).toEqual({transactionId: 'tx3'})
116
+ })
117
+
118
+ it('applies edit actions using an updater function for the entire document', async () => {
119
+ const currentDoc = {...doc, foo: 'bar', extra: 'old'}
120
+ const getCurrent = vi.fn().mockReturnValue(currentDoc)
121
+ const subscribe = vi.fn().mockReturnValue(vi.fn())
122
+ vi.mocked(getDocumentState).mockReturnValue({
123
+ getCurrent,
124
+ subscribe,
125
+ } as unknown as StateSource<SanityDocument>)
126
+
127
+ const apply = vi.fn().mockResolvedValue({transactionId: 'tx4'})
128
+ vi.mocked(useApplyActions).mockReturnValue(apply)
129
+
130
+ const {result} = renderHook(() => useEditDocument(docHandle))
131
+ const promise = result.current((prevDoc) => ({...prevDoc, foo: 'baz'}))
132
+ expect(apply).toHaveBeenCalledWith([editDocument(docHandle, {set: {foo: 'baz'}})])
133
+ const actionsResult = await promise
134
+ expect(actionsResult).toEqual({transactionId: 'tx4'})
135
+ })
136
+
92
137
  it('throws an error if next value is not an object', () => {
93
138
  const getCurrent = vi.fn().mockReturnValue(doc)
94
139
  const subscribe = vi.fn().mockReturnValue(vi.fn())
@@ -100,8 +145,8 @@ describe('useEditDocument hook', () => {
100
145
  const fakeApply = vi.fn()
101
146
  vi.mocked(useApplyActions).mockReturnValue(fakeApply)
102
147
 
103
- const {result} = renderHook(() => useEditDocument('doc1'))
104
- expect(() => result.current('notAnObject' as unknown as Partial<SanityDocument>)).toThrowError(
148
+ const {result} = renderHook(() => useEditDocument(docHandle))
149
+ expect(() => result.current('notAnObject' as unknown as SanityDocument)).toThrowError(
105
150
  'No path was provided to `useEditDocument` and the value provided was not a document object.',
106
151
  )
107
152
  })
@@ -122,7 +167,7 @@ describe('useEditDocument hook', () => {
122
167
  // Render the hook and capture the thrown promise.
123
168
  const {result} = renderHook(() => {
124
169
  try {
125
- return useEditDocument('doc1')
170
+ return useEditDocument(docHandle)
126
171
  } catch (e) {
127
172
  return e
128
173
  }
@@ -3,6 +3,7 @@ import {
3
3
  type DocumentHandle,
4
4
  editDocument,
5
5
  getDocumentState,
6
+ getResourceId,
6
7
  type JsonMatch,
7
8
  type JsonMatchPath,
8
9
  resolveDocument,
@@ -15,53 +16,180 @@ import {useApplyActions} from './useApplyActions'
15
16
 
16
17
  const ignoredKeys = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']
17
18
 
18
- /** @beta */
19
+ type Updater<TValue> = TValue | ((nextValue: TValue) => TValue)
20
+
21
+ /**
22
+ *
23
+ * @beta
24
+ *
25
+ * ## useEditDocument(doc, path)
26
+ * Edit a nested value within a document
27
+ *
28
+ * @category Documents
29
+ * @param doc - The document to be edited; either as a document handle or the document’s ID a string
30
+ * @param path - The path to the nested value to be edited
31
+ * @returns A function to update the nested value. Accepts either a new value, or an updater function that exposes the previous value and returns a new value.
32
+ * @example Update a document’s name by providing the new value directly
33
+ * ```
34
+ * const handle = { _id: 'documentId', _type: 'documentType' }
35
+ * const name = useDocument(handle, 'name')
36
+ * const editName = useEditDocument(handle, 'name')
37
+ *
38
+ * function handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
39
+ * editName(event.target.value)
40
+ * }
41
+ *
42
+ * return (
43
+ * <input type='text' value={name} onChange={handleNameChange} />
44
+ * )
45
+ * ```
46
+ *
47
+ * @example Update a count on a document by providing an updater function
48
+ * ```
49
+ * const handle = { _id: 'documentId', _type: 'documentType' }
50
+ * const count = useDocument(handle, 'count')
51
+ * const editCount = useEditDocument(handle, 'count')
52
+ *
53
+ * function incrementCount() {
54
+ * editCount(previousCount => previousCount + 1)
55
+ * }
56
+ *
57
+ * return (
58
+ * <>
59
+ * <button onClick={incrementCount}>
60
+ * Increment
61
+ * </button>
62
+ * Current count: {count}
63
+ * </>
64
+ * )
65
+ * ```
66
+ */
19
67
  export function useEditDocument<
20
68
  TDocument extends SanityDocument,
21
69
  TPath extends JsonMatchPath<TDocument>,
22
70
  >(
23
- doc: string | DocumentHandle<TDocument>,
71
+ doc: DocumentHandle<TDocument>,
24
72
  path: TPath,
25
- ): (nextValue: JsonMatch<TDocument, TPath>) => Promise<ActionsResult<TDocument>>
26
- /** @beta */
73
+ ): (nextValue: Updater<JsonMatch<TDocument, TPath>>) => Promise<ActionsResult<TDocument>>
74
+
75
+ /**
76
+ *
77
+ * @beta
78
+ *
79
+ * ## useEditDocument(doc)
80
+ * Edit an entire document
81
+ * @param doc - The document to be edited; either as a document handle or the document’s ID a string. If you pass a `DocumentHandle` with a `resourceId` (in the format of `document:projectId.dataset:documentId`)
82
+ * the document will be read from the specified Sanity project and dataset that is included in the handle. If no `resourceId` is provided, the default project and dataset from your `SanityApp` configuration will be used.
83
+ * @returns A function to update the document state. Accepts either a new document state, or an updater function that exposes the previous document state and returns the new document state.
84
+ * @example
85
+ * ```
86
+ * const myDocumentHandle = { _id: 'documentId', _type: 'documentType' }
87
+ *
88
+ * const myDocument = useDocument(myDocumentHandle)
89
+ * const { title, price } = myDocument
90
+ *
91
+ * const editMyDocument = useEditDocument(myDocumentHandle)
92
+ *
93
+ * function handleFieldChange(e: React.ChangeEvent<HTMLInputElement>) {
94
+ * const {name, value} = e.currentTarget
95
+ * // Use an updater function to update the document state based on the previous state
96
+ * editMyDocument(previousDocument => ({
97
+ * ...previousDocument,
98
+ * [name]: value
99
+ * }))
100
+ * }
101
+ *
102
+ * function handleSaleChange(e: React.ChangeEvent<HTMLInputElement>) {
103
+ * const { checked } = e.currentTarget
104
+ * if (checked) {
105
+ * // Use an updater function to add a new salePrice field;
106
+ * // set it at a 20% discount off the normal price
107
+ * editMyDocument(previousDocument => ({
108
+ * ...previousDocument,
109
+ * salePrice: previousDocument.price * 0.8,
110
+ * }))
111
+ * } else {
112
+ * // Get the document state without the salePrice field
113
+ * const { salePrice, ...rest } = myDocument
114
+ * // Update the document state to remove the salePrice field
115
+ * editMyDocument(rest)
116
+ * }
117
+ * }
118
+ *
119
+ * return (
120
+ * <>
121
+ * <form onSubmit={e => e.preventDefault()}>
122
+ * <input name='title' type='text' value={title} onChange={handleFieldChange} />
123
+ * <input name='price' type='number' value={price} onChange={handleFieldChange} />
124
+ * <input
125
+ * name='salePrice'
126
+ * type='checkbox'
127
+ * checked={Object(myDocument).hasOwnProperty('salePrice')}
128
+ * onChange={handleSaleChange}
129
+ * />
130
+ * </form>
131
+ * <pre><code>
132
+ * {JSON.stringify(myDocument, null, 2)}
133
+ * </code></pre>
134
+ * </>
135
+ * )
136
+ * ```
137
+ */
27
138
  export function useEditDocument<TDocument extends SanityDocument>(
28
- doc: string | DocumentHandle<TDocument>,
29
- ): (nextValue: Partial<TDocument>) => Promise<ActionsResult<TDocument>>
30
- /** @beta */
139
+ doc: DocumentHandle<TDocument>,
140
+ ): (nextValue: Updater<TDocument>) => Promise<ActionsResult<TDocument>>
141
+
142
+ /**
143
+ *
144
+ * @beta
145
+ *
146
+ * Enables editing of a document’s state.
147
+ * When called with a `path` argument, the hook will return a function for updating a nested value.
148
+ * When called without a `path` argument, the hook will return a function for updating the entire document.
149
+ */
31
150
  export function useEditDocument(
32
- doc: string | DocumentHandle,
151
+ doc: DocumentHandle,
33
152
  path?: string,
34
- ): (nextValue: unknown) => Promise<ActionsResult> {
35
- const documentId = typeof doc === 'string' ? doc : doc._id
36
- const instance = useSanityInstance()
37
- const apply = useApplyActions()
153
+ ): (updater: Updater<unknown>) => Promise<ActionsResult> {
154
+ const resourceId = getResourceId(doc.resourceId)!
155
+ const documentId = doc._id
156
+ const instance = useSanityInstance(resourceId)
157
+ const apply = useApplyActions(resourceId)
38
158
  const isDocumentReady = useCallback(
39
159
  () => getDocumentState(instance, documentId).getCurrent() !== undefined,
40
160
  [instance, documentId],
41
161
  )
42
162
  if (!isDocumentReady()) throw resolveDocument(instance, documentId)
43
163
 
44
- return useCallback(
45
- (next: unknown) => {
46
- if (path) {
47
- return apply(editDocument(documentId, {set: {[path]: next}}))
48
- }
164
+ return (updater: Updater<unknown>) => {
165
+ if (path) {
166
+ const nextValue =
167
+ typeof updater === 'function'
168
+ ? updater(getDocumentState(instance, documentId, path).getCurrent())
169
+ : updater
49
170
 
50
- const current = getDocumentState(instance, documentId).getCurrent()
171
+ return apply(editDocument(doc, {set: {[path]: nextValue}}))
172
+ }
51
173
 
52
- if (typeof next !== 'object' || !next) {
53
- throw new Error(
54
- `No path was provided to \`useEditDocument\` and the value provided was not a document object.`,
55
- )
56
- }
174
+ const current = getDocumentState(instance, documentId).getCurrent()
175
+ const nextValue = typeof updater === 'function' ? updater(current) : updater
57
176
 
58
- const editActions = Object.entries(next)
59
- .filter(([key]) => !ignoredKeys.includes(key))
60
- .filter(([key, value]) => current?.[key] !== value)
61
- .map(([key, value]) => editDocument(documentId, {set: {[key]: value}}))
177
+ if (typeof nextValue !== 'object' || !nextValue) {
178
+ throw new Error(
179
+ `No path was provided to \`useEditDocument\` and the value provided was not a document object.`,
180
+ )
181
+ }
62
182
 
63
- return apply(editActions)
64
- },
65
- [apply, documentId, instance, path],
66
- )
183
+ const allKeys = Object.keys({...current, ...nextValue})
184
+ const editActions = allKeys
185
+ .filter((key) => !ignoredKeys.includes(key))
186
+ .filter((key) => current?.[key] !== nextValue[key])
187
+ .map((key) =>
188
+ key in nextValue
189
+ ? editDocument(doc, {set: {[key]: nextValue[key]}})
190
+ : editDocument(doc, {unset: [key]}),
191
+ )
192
+
193
+ return apply(editActions)
194
+ }
67
195
  }