@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2

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 (168) hide show
  1. package/README.md +103 -155
  2. package/dist/index.d.ts +2488 -2019
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +4435 -1529
  5. package/dist/index.js.map +1 -1
  6. package/package.json +55 -59
  7. package/src/_exports/index.ts +2 -4
  8. package/src/_exports/sdk-react.ts +30 -18
  9. package/src/components/SDKProvider.test.tsx +173 -32
  10. package/src/components/SDKProvider.tsx +87 -28
  11. package/src/components/SanityApp.test.tsx +68 -12
  12. package/src/components/SanityApp.tsx +93 -87
  13. package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
  14. package/src/components/auth/AuthBoundary.test.tsx +7 -7
  15. package/src/components/auth/AuthBoundary.tsx +29 -6
  16. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  17. package/src/components/auth/LoginError.test.tsx +191 -5
  18. package/src/components/auth/LoginError.tsx +104 -61
  19. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  20. package/src/components/errors/ChunkLoadError.tsx +56 -0
  21. package/src/components/errors/CorsErrorComponent.tsx +2 -2
  22. package/src/components/errors/chunkReloadStorage.ts +57 -0
  23. package/src/config/handles.ts +29 -27
  24. package/src/constants.ts +5 -0
  25. package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
  26. package/src/context/ComlinkTokenRefresh.tsx +3 -5
  27. package/src/context/DefaultResourceContext.ts +3 -3
  28. package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
  29. package/src/context/OrganizationResourcesProvider.tsx +111 -0
  30. package/src/context/PerspectiveContext.ts +3 -3
  31. package/src/context/ProjectContext.ts +15 -0
  32. package/src/context/ResourceProvider.test.tsx +93 -90
  33. package/src/context/ResourceProvider.tsx +62 -93
  34. package/src/context/SDKStudioContext.test.tsx +28 -33
  35. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  36. package/src/context/SanityInstanceProvider.tsx +71 -0
  37. package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
  38. package/src/context/WorkbenchTokenRefresh.tsx +61 -0
  39. package/src/context/renderSanityApp.test.tsx +151 -49
  40. package/src/context/renderSanityApp.tsx +12 -8
  41. package/src/context/workbenchToken.ts +63 -0
  42. package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
  43. package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
  44. package/src/hooks/access/useCheckPermissions.ts +24 -0
  45. package/src/hooks/agent/agentActions.test.tsx +2 -2
  46. package/src/hooks/agent/agentActions.ts +52 -72
  47. package/src/hooks/agent/useAgentResourceContext.ts +1 -1
  48. package/src/hooks/applications/useApplication.test-d.ts +30 -0
  49. package/src/hooks/applications/useApplication.ts +22 -0
  50. package/src/hooks/applications/useApplications.test-d.ts +31 -0
  51. package/src/hooks/applications/useApplications.ts +25 -0
  52. package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
  53. package/src/hooks/applications/useDeleteApplication.ts +11 -0
  54. package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
  55. package/src/hooks/applications/useUpdateApplication.ts +11 -0
  56. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
  57. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
  58. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  59. package/src/hooks/client/useClient.test.tsx +1 -4
  60. package/src/hooks/client/useClient.ts +1 -0
  61. package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
  62. package/src/hooks/comlink/useFrameConnection.ts +1 -1
  63. package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
  64. package/src/hooks/comlink/useWindowConnection.ts +2 -3
  65. package/src/hooks/comments/useCommentActions.test.tsx +150 -0
  66. package/src/hooks/comments/useCommentActions.ts +109 -0
  67. package/src/hooks/comments/useCommentList.ts +79 -0
  68. package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
  69. package/src/hooks/comments/useCommentThreads.ts +73 -0
  70. package/src/hooks/comments/useComments.test.tsx +242 -0
  71. package/src/hooks/comments/useComments.ts +59 -0
  72. package/src/hooks/context/useResource.test.tsx +32 -0
  73. package/src/hooks/context/useResource.ts +24 -0
  74. package/src/hooks/context/useSanityInstance.test.tsx +18 -1
  75. package/src/hooks/context/useSanityInstance.ts +6 -6
  76. package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
  77. package/src/hooks/dashboard/useManageFavorite.ts +11 -17
  78. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
  79. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
  80. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
  81. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -2
  82. package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
  83. package/src/hooks/dashboard/useWindowTitle.ts +114 -0
  84. package/src/hooks/datasets/useDatasets.test.tsx +116 -0
  85. package/src/hooks/datasets/useDatasets.ts +33 -13
  86. package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
  87. package/src/hooks/document/useApplyDocumentActions.ts +66 -48
  88. package/src/hooks/document/useCreateDocument.test.tsx +83 -0
  89. package/src/hooks/document/useCreateDocument.ts +117 -0
  90. package/src/hooks/document/useDocument.test.tsx +37 -8
  91. package/src/hooks/document/useDocument.ts +131 -66
  92. package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
  93. package/src/hooks/document/useDocumentEvent.ts +16 -10
  94. package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
  95. package/src/hooks/document/useDocumentPermissions.ts +71 -39
  96. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
  97. package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
  98. package/src/hooks/document/useEditDocument.test.tsx +21 -14
  99. package/src/hooks/document/useEditDocument.ts +163 -17
  100. package/src/hooks/documents/useDocuments.test.tsx +64 -44
  101. package/src/hooks/documents/useDocuments.ts +31 -20
  102. package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
  103. package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
  104. package/src/hooks/helpers/createFetcherHook.ts +69 -0
  105. package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
  106. package/src/hooks/helpers/createMutationHook.tsx +93 -0
  107. package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
  108. package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
  109. package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
  110. package/src/hooks/helpers/useApplyActions.ts +68 -0
  111. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +226 -0
  112. package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
  113. package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
  114. package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
  115. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  116. package/src/hooks/installations/useInstallation.test-d.ts +19 -0
  117. package/src/hooks/installations/useInstallation.ts +22 -0
  118. package/src/hooks/installations/useInstallations.test-d.ts +26 -0
  119. package/src/hooks/installations/useInstallations.ts +25 -0
  120. package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
  121. package/src/hooks/organizations/useOrganization.test.ts +63 -0
  122. package/src/hooks/organizations/useOrganization.ts +34 -0
  123. package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
  124. package/src/hooks/organizations/useOrganizations.test.ts +63 -0
  125. package/src/hooks/organizations/useOrganizations.ts +39 -0
  126. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
  127. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +35 -22
  128. package/src/hooks/presence/usePresence.test.tsx +0 -15
  129. package/src/hooks/presence/usePresence.ts +22 -20
  130. package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
  131. package/src/hooks/presence/usePresenceForDocument.ts +104 -0
  132. package/src/hooks/presence/useReportPresence.test.tsx +202 -0
  133. package/src/hooks/presence/useReportPresence.ts +170 -0
  134. package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
  135. package/src/hooks/preview/useDocumentPreview.tsx +12 -14
  136. package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
  137. package/src/hooks/projection/useDocumentProjection.ts +81 -2
  138. package/src/hooks/projects/useProject.test-d.ts +56 -0
  139. package/src/hooks/projects/useProject.test.tsx +120 -0
  140. package/src/hooks/projects/useProject.ts +42 -43
  141. package/src/hooks/projects/useProjects.test-d.ts +60 -0
  142. package/src/hooks/projects/useProjects.test.ts +45 -98
  143. package/src/hooks/projects/useProjects.ts +17 -32
  144. package/src/hooks/query/useQuery.test.tsx +6 -5
  145. package/src/hooks/query/useQuery.ts +94 -17
  146. package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
  147. package/src/hooks/releases/useActiveReleases.ts +28 -24
  148. package/src/hooks/releases/useAllReleases.test.tsx +93 -0
  149. package/src/hooks/releases/useAllReleases.ts +62 -0
  150. package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
  151. package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
  152. package/src/hooks/releases/usePerspective.test.tsx +18 -10
  153. package/src/hooks/releases/usePerspective.ts +13 -17
  154. package/src/hooks/users/useUser.test.tsx +3 -9
  155. package/src/hooks/users/useUser.ts +4 -8
  156. package/src/hooks/users/useUsers.test.tsx +103 -7
  157. package/src/hooks/users/useUsers.ts +38 -5
  158. package/src/utils/resolveOrgResources.test.ts +111 -0
  159. package/src/utils/resolveOrgResources.ts +69 -0
  160. package/src/hooks/context/useDefaultResource.test.tsx +0 -25
  161. package/src/hooks/context/useDefaultResource.ts +0 -30
  162. package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
  163. package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
  164. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
  165. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
  166. package/src/hooks/datasets/useDatasets.test.ts +0 -66
  167. package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
  168. package/src/hooks/projects/useProject.test.ts +0 -79
@@ -1,12 +1,13 @@
1
1
  import {
2
- type DocumentHandle as StrictDocumentHandle,
2
+ type DocumentHandle,
3
+ type DocumentOptions,
3
4
  getDocumentSyncStatus,
4
5
  resolveDocument,
5
6
  type SanityInstance,
6
7
  type StateSource,
7
8
  } from '@sanity/sdk'
9
+ import {identity} from 'rxjs'
8
10
 
9
- import {type DocumentHandle} from '../../config/handles'
10
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
11
12
  import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
12
13
 
@@ -15,9 +16,9 @@ type UseDocumentSyncStatus = {
15
16
  * Exposes the document's sync status between local and remote document states.
16
17
  *
17
18
  * @category Documents
18
- * @param doc - The document handle to get sync status for. If you pass a `resource` in the handle,
19
- * the document will be read from the specified resource. If no `resource` is provided,
20
- * the resource will be resolved from context.
19
+ * @param doc - The document handle to get sync status for. If you pass a `DocumentHandle` with specified `projectId` and `dataset`,
20
+ * the document will be read from the specified Sanity project and dataset that is included in the handle. If no `projectId` or `dataset` is provided,
21
+ * the document will use the nearest instance from context.
21
22
  * @returns `true` if local changes are synced with remote, `false` if changes are pending. Note: Suspense handles loading states.
22
23
  * @example Show sync status indicator
23
24
  * ```tsx
@@ -49,18 +50,21 @@ type UseDocumentSyncStatus = {
49
50
  const useDocumentSyncStatusValue = createStateSourceHook({
50
51
  getState: getDocumentSyncStatus as (
51
52
  instance: SanityInstance,
52
- doc: StrictDocumentHandle,
53
+ doc: DocumentHandle,
53
54
  ) => StateSource<boolean>,
54
- shouldSuspend: (instance, doc: StrictDocumentHandle) =>
55
+ shouldSuspend: (instance, doc: DocumentHandle) =>
55
56
  getDocumentSyncStatus(instance, doc).getCurrent() === undefined,
56
- suspender: (instance, doc: StrictDocumentHandle) => resolveDocument(instance, doc),
57
+ suspender: (instance, doc: DocumentHandle) => resolveDocument(instance, doc),
58
+ getConfig: identity,
57
59
  })
58
60
 
59
61
  /**
60
62
  * @public
61
63
  * @function
62
64
  */
63
- export const useDocumentSyncStatus: UseDocumentSyncStatus = (options: DocumentHandle) => {
65
+ export const useDocumentSyncStatus: UseDocumentSyncStatus = (
66
+ options: DocumentOptions<string | undefined>,
67
+ ) => {
64
68
  const normalizedOptions = useNormalizedResourceOptions(options)
65
69
  return useDocumentSyncStatusValue(normalizedOptions)
66
70
  }
@@ -39,12 +39,11 @@ const doc = {
39
39
  const docHandle = createDocumentHandle({
40
40
  documentId: 'doc1',
41
41
  documentType: 'book',
42
- resource: {projectId: 'test-project', dataset: 'test-dataset'},
43
42
  })
44
- const normalizedDocHandle = {
45
- documentId: 'doc1',
46
- documentType: 'book',
47
- resource: {projectId: 'test-project', dataset: 'test-dataset'},
43
+
44
+ const normalizedDoc = {
45
+ ...docHandle,
46
+ resource: {projectId: 'test', dataset: 'test'},
48
47
  }
49
48
 
50
49
  // Define a single generic TestDocument type
@@ -55,6 +54,16 @@ interface Book extends SanityDocument {
55
54
  title?: string
56
55
  }
57
56
 
57
+ // Scope the TestDocument type to the project/datasets used in tests
58
+ type AllTestSchemaTypes = Book
59
+
60
+ // Augment the 'groq' module
61
+ declare module 'groq' {
62
+ interface SanitySchemas {
63
+ 'default:default': AllTestSchemaTypes
64
+ }
65
+ }
66
+
58
67
  describe('useEditDocument hook', () => {
59
68
  beforeEach(() => {
60
69
  vi.clearAllMocks()
@@ -73,8 +82,8 @@ describe('useEditDocument hook', () => {
73
82
 
74
83
  const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
75
84
  const promise = result.current('newValue')
76
- expect(editDocument).toHaveBeenCalledWith(normalizedDocHandle, {set: {foo: 'newValue'}})
77
- expect(apply).toHaveBeenCalledWith(editDocument(normalizedDocHandle, {set: {foo: 'newValue'}}))
85
+ expect(editDocument).toHaveBeenCalledWith(normalizedDoc, {set: {foo: 'newValue'}})
86
+ expect(apply).toHaveBeenCalledWith(editDocument(normalizedDoc, {set: {foo: 'newValue'}}))
78
87
  const actionsResult = await promise
79
88
  expect(actionsResult).toEqual({transactionId: 'tx1'})
80
89
  })
@@ -94,7 +103,7 @@ describe('useEditDocument hook', () => {
94
103
 
95
104
  const {result} = renderHook(() => useEditDocument(docHandle))
96
105
  const promise = result.current({...doc, foo: 'baz', extra: 'old', _id: 'doc1'})
97
- expect(apply).toHaveBeenCalledWith([editDocument(normalizedDocHandle, {set: {foo: 'baz'}})])
106
+ expect(apply).toHaveBeenCalledWith([editDocument(normalizedDoc, {set: {foo: 'baz'}})])
98
107
  const actionsResult = await promise
99
108
  expect(actionsResult).toEqual({transactionId: 'tx2'})
100
109
  })
@@ -112,10 +121,8 @@ describe('useEditDocument hook', () => {
112
121
 
113
122
  const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
114
123
  const promise = result.current((prev: unknown) => `${prev}Updated`) // 'bar' becomes 'barUpdated'
115
- expect(editDocument).toHaveBeenCalledWith(normalizedDocHandle, {set: {foo: 'barUpdated'}})
116
- expect(apply).toHaveBeenCalledWith(
117
- editDocument(normalizedDocHandle, {set: {foo: 'barUpdated'}}),
118
- )
124
+ expect(editDocument).toHaveBeenCalledWith(normalizedDoc, {set: {foo: 'barUpdated'}})
125
+ expect(apply).toHaveBeenCalledWith(editDocument(normalizedDoc, {set: {foo: 'barUpdated'}}))
119
126
  const actionsResult = await promise
120
127
  expect(actionsResult).toEqual({transactionId: 'tx3'})
121
128
  })
@@ -133,8 +140,8 @@ describe('useEditDocument hook', () => {
133
140
  vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
134
141
 
135
142
  const {result} = renderHook(() => useEditDocument(docHandle))
136
- const promise = result.current((prevDoc: Record<string, unknown>) => ({...prevDoc, foo: 'baz'}))
137
- expect(apply).toHaveBeenCalledWith([editDocument(normalizedDocHandle, {set: {foo: 'baz'}})])
143
+ const promise = result.current((prevDoc: Book) => ({...prevDoc, foo: 'baz'}))
144
+ expect(apply).toHaveBeenCalledWith([editDocument(normalizedDoc, {set: {foo: 'baz'}})])
138
145
  const actionsResult = await promise
139
146
  expect(actionsResult).toEqual({transactionId: 'tx4'})
140
147
  })
@@ -1,44 +1,86 @@
1
- import {type ActionsResult, editDocument, getDocumentState, resolveDocument} from '@sanity/sdk'
1
+ import {
2
+ type ActionsResult,
3
+ type DocumentOptions,
4
+ editDocument,
5
+ getDocumentState,
6
+ type JsonMatch,
7
+ resolveDocument,
8
+ } from '@sanity/sdk'
9
+ import {type SanityDocument} from 'groq'
2
10
  import {useCallback} from 'react'
3
11
 
4
- import {type DocumentHandle} from '../../config/handles'
5
12
  import {useSanityInstance} from '../context/useSanityInstance'
6
13
  import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
14
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
7
15
  import {useApplyDocumentActions} from './useApplyDocumentActions'
8
16
 
9
17
  const ignoredKeys = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']
10
18
 
11
19
  type Updater<TValue> = TValue | ((currentValue: TValue) => TValue)
12
20
 
13
- /** React-layer edit document options: DocumentHandle with optional path */
14
- type EditDocumentOptions<TPath extends string | undefined = undefined> = DocumentHandle & {
15
- path?: TPath
16
- }
21
+ // Overload 1: No path, relies on Typegen
22
+ /**
23
+ * @public
24
+ * Edit an entire document, relying on Typegen for the type.
25
+ *
26
+ * @param options - Document options including `documentId`, `documentType`, and optionally `projectId`/`dataset`.
27
+ * @returns A stable function to update the document state. Accepts either the new document state or an updater function `(currentValue) => nextValue`.
28
+ * Returns a promise resolving to the {@link ActionsResult}.
29
+ */
30
+ export function useEditDocument<
31
+ TDocumentType extends string = string,
32
+ TDataset extends string = string,
33
+ TProjectId extends string = string,
34
+ >(
35
+ options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
36
+ ): (
37
+ nextValue: Updater<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>,
38
+ ) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
39
+
40
+ // Overload 2: Path provided, relies on Typegen
41
+ /**
42
+ * @public
43
+ * Edit a specific path within a document, relying on Typegen for the type.
44
+ *
45
+ * @param options - Document options including `documentId`, `documentType`, `path`, and optionally `projectId`/`dataset`.
46
+ * @returns A stable function to update the value at the specified path. Accepts either the new value or an updater function `(currentValue) => nextValue`.
47
+ * Returns a promise resolving to the {@link ActionsResult}.
48
+ */
49
+ export function useEditDocument<
50
+ TPath extends string = string,
51
+ TDocumentType extends string = string,
52
+ TDataset extends string = string,
53
+ TProjectId extends string = string,
54
+ >(
55
+ options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
56
+ ): (
57
+ nextValue: Updater<JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath>>,
58
+ ) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
17
59
 
18
- // Overload 1: Explicit type, no path
60
+ // Overload 3: Explicit type, no path
19
61
  /**
20
62
  * @public
21
63
  * Edit an entire document with an explicit type `TData`.
22
64
  *
23
- * @param options - Document options including `documentId` and optionally `resource` or `resourceName`.
65
+ * @param options - Document options including `documentId` and optionally `projectId`/`dataset`.
24
66
  * @returns A stable function to update the document state. Accepts either the new document state (`TData`) or an updater function `(currentValue: TData) => nextValue: TData`.
25
67
  * Returns a promise resolving to the {@link ActionsResult}.
26
68
  */
27
69
  export function useEditDocument<TData>(
28
- options: EditDocumentOptions<undefined>,
70
+ options: DocumentOptions<undefined>,
29
71
  ): (nextValue: Updater<TData>) => Promise<ActionsResult>
30
72
 
31
- // Overload 2: Explicit type, path provided
73
+ // Overload 4: Explicit type, path provided
32
74
  /**
33
75
  * @public
34
76
  * Edit a specific path within a document with an explicit type `TData`.
35
77
  *
36
- * @param options - Document options including `documentId`, `path`, and optionally `resource` or `resourceName`.
78
+ * @param options - Document options including `documentId`, `path`, and optionally `projectId`/`dataset`.
37
79
  * @returns A stable function to update the value at the specified path. Accepts either the new value (`TData`) or an updater function `(currentValue: TData) => nextValue: TData`.
38
80
  * Returns a promise resolving to the {@link ActionsResult}.
39
81
  */
40
82
  export function useEditDocument<TData>(
41
- options: EditDocumentOptions<string>,
83
+ options: DocumentOptions<string>,
42
84
  ): (nextValue: Updater<TData>) => Promise<ActionsResult>
43
85
 
44
86
  /**
@@ -54,9 +96,11 @@ export function useEditDocument<TData>(
54
96
  * - Integrating with the active {@link SanityInstance} context.
55
97
  * - Utilizing `useApplyDocumentActions` internally for optimistic updates and transaction handling.
56
98
  *
57
- * It offers overloads for flexibility:
58
- * 1. **Explicit Type (Full Document):** Edit the entire document with a manually specified type.
59
- * 2. **Explicit Type (Specific Path):** Edit a specific field with a manually specified type.
99
+ * It offers several overloads for flexibility:
100
+ * 1. **Typegen (Full Document):** Edit the entire document, inferring types from your schema.
101
+ * 2. **Typegen (Specific Path):** Edit a specific field, inferring types.
102
+ * 3. **Explicit Type (Full Document):** Edit the entire document with a manually specified type.
103
+ * 4. **Explicit Type (Specific Path):** Edit a specific field with a manually specified type.
60
104
  *
61
105
  * **LiveEdit Documents:**
62
106
  * For documents using {@link DocumentHandle.liveEdit | liveEdit mode} (set via `liveEdit: true` in the document handle), edits are applied directly to the published document without creating a draft.
@@ -64,7 +108,87 @@ export function useEditDocument<TData>(
64
108
  * This hook relies on the document state being loaded. If the document is not yet available
65
109
  * (e.g., during initial load), the component using this hook will suspend.
66
110
  *
67
- * @example Basic Usage with Explicit Types (Full Document)
111
+ * @example Basic Usage (Typegen, Full Document)
112
+ * ```tsx
113
+ * import {useCallback} from 'react';
114
+ * import {useEditDocument, useDocument, type DocumentHandle} from '@sanity/sdk-react'
115
+ *
116
+ * // Assume 'product' schema has a 'title' field (string)
117
+ * interface ProductEditorProps {
118
+ * productHandle: DocumentHandle<'product'> // Typegen infers 'product' type
119
+ * }
120
+ *
121
+ * function ProductEditor({ productHandle }: ProductEditorProps) {
122
+ * // Fetch the document to display its current state (optional)
123
+ * const {data: product} = useDocument(productHandle);
124
+ * // Get the edit function for the full document
125
+ * const editProduct = useEditDocument(productHandle);
126
+ *
127
+ * // Use useCallback for stable event handlers
128
+ * const handleTitleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
129
+ * const newTitle = event.target.value;
130
+ * // Use the functional updater for safe partial updates
131
+ * editProduct(prev => ({
132
+ * ...prev,
133
+ * title: newTitle,
134
+ * })).
135
+ * }, [editProduct]);
136
+ *
137
+ * return (
138
+ * <div>
139
+ * <label>
140
+ * Product Title:
141
+ * <input
142
+ * type="text"
143
+ * value={product?.title ?? ''}
144
+ * onChange={handleTitleChange}
145
+ * />
146
+ * </label>
147
+ * </div>
148
+ * );
149
+ * }
150
+ * ```
151
+ *
152
+ * @example Editing a Specific Path (Typegen)
153
+ * ```tsx
154
+ * import React, { useCallback } from 'react';
155
+ * import {useEditDocument, useDocument, type DocumentHandle, type DocumentOptions} from '@sanity/sdk-react'
156
+ *
157
+ * // Assume 'product' schema has a 'price' field (number)
158
+ * interface ProductPriceEditorProps {
159
+ * productHandle: DocumentHandle<'product'>;
160
+ * }
161
+ *
162
+ * function ProductPriceEditor({ productHandle }: ProductPriceEditorProps) {
163
+ * // Construct DocumentOptions internally, combining the handle and a hardcoded path
164
+ * const priceOptions {
165
+ * ...productHandle,
166
+ * path: 'price', // Hardcode the path to edit
167
+ * };
168
+ *
169
+ * // Fetch the current price to display it
170
+ * const {data: currentPrice} = useDocument(priceOptions);
171
+ * // Get the edit function for the specific path 'price'
172
+ * const editPrice = useEditDocument(priceOptions);
173
+ *
174
+ * const handleSetFixedPrice = useCallback(() => {
175
+ * // Update the price directly to a hardcoded value
176
+ * editPrice(99.99)
177
+ * }, [editPrice]);
178
+ *
179
+ * return (
180
+ * <div>
181
+ * <p>Current Price: {currentPrice}</p>
182
+ * <button onClick={handleSetFixedPrice}>
183
+ * Set Price to $99.99
184
+ * </button>
185
+ * </div>
186
+ * );
187
+ * }
188
+ *
189
+ * ```
190
+ *
191
+ * @example Usage with Explicit Types (Full Document)
68
192
  * ```tsx
69
193
  * import React, { useCallback } from 'react';
70
194
  * import {useEditDocument, useDocument, type DocumentHandle, type SanityDocument} from '@sanity/sdk-react'
@@ -136,12 +260,34 @@ export function useEditDocument<TData>(
136
260
  * }
137
261
  *
138
262
  * ```
263
+ *
264
+ * @example Edit a document in a release
265
+ * ```tsx
266
+ * import {useEditDocument} from '@sanity/sdk-react'
267
+ *
268
+ * function EditArticleInRelease({documentId}: {documentId: string}) {
269
+ * // Use the document's plain ID — not `versions.<releaseName>.<id>`.
270
+ * // The document must already exist in the release (added via `createDocument` first).
271
+ * const editArticle = useEditDocument({
272
+ * documentId,
273
+ * documentType: 'article',
274
+ * perspective: {releaseName: 'summer-drop'},
275
+ * })
276
+ *
277
+ * return (
278
+ * <button onClick={() => editArticle(prev => ({...prev, title: 'Updated for release'}))}>
279
+ * Edit in Release
280
+ * </button>
281
+ * )
282
+ * }
283
+ * ```
139
284
  */
140
285
  export function useEditDocument({
141
286
  path,
142
287
  ...doc
143
- }: EditDocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
288
+ }: DocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
144
289
  const instance = useSanityInstance()
290
+ trackHookUsage(instance, 'useEditDocument')
145
291
  const normalizedDoc = useNormalizedResourceOptions(doc)
146
292
 
147
293
  const apply = useApplyDocumentActions()
@@ -1,10 +1,8 @@
1
- import {type DatasetResource} from '@sanity/sdk'
1
+ import {act, renderHook} from '@testing-library/react'
2
2
  import {evaluateSync, parse, toJS} from 'groq-js'
3
3
  import {describe, vi} from 'vitest'
4
4
 
5
- import {act, renderHook} from '../../../test/test-utils'
6
5
  import {ResourceProvider} from '../../context/ResourceProvider'
7
- import {ResourcesContext} from '../../context/ResourcesContext'
8
6
  import {useQuery} from '../query/useQuery'
9
7
  import {useDocuments} from './useDocuments'
10
8
 
@@ -79,13 +77,25 @@ describe('useDocuments', () => {
79
77
 
80
78
  it('should respect custom page size', () => {
81
79
  const customBatchSize = 2
82
- const {result} = renderHook(() => useDocuments({batchSize: customBatchSize}))
80
+ const {result} = renderHook(() => useDocuments({batchSize: customBatchSize}), {
81
+ wrapper: ({children}) => (
82
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
83
+ {children}
84
+ </ResourceProvider>
85
+ ),
86
+ })
83
87
 
84
88
  expect(result.current.data.length).toBe(customBatchSize)
85
89
  })
86
90
 
87
91
  it('should filter by document type', () => {
88
- const {result} = renderHook(() => useDocuments({filter: '_type == "movie"'}))
92
+ const {result} = renderHook(() => useDocuments({filter: '_type == "movie"'}), {
93
+ wrapper: ({children}) => (
94
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
95
+ {children}
96
+ </ResourceProvider>
97
+ ),
98
+ })
89
99
 
90
100
  expect(result.current.data.every((doc) => doc.documentType === 'movie')).toBe(true)
91
101
  expect(result.current.count).toBe(5) // 5 movies in the dataset
@@ -93,18 +103,32 @@ describe('useDocuments', () => {
93
103
 
94
104
  // groq-js doesn't support search filters yet
95
105
  it.skip('should apply search filter', () => {
96
- const {result} = renderHook(() => useDocuments({search: 'inter'}))
106
+ const {result} = renderHook(() => useDocuments({search: 'inter'}), {
107
+ wrapper: ({children}) => (
108
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
109
+ {children}
110
+ </ResourceProvider>
111
+ ),
112
+ })
97
113
 
98
114
  // Should match "Interstellar"
99
115
  expect(result.current.data.some((doc) => doc.documentId === 'movie3')).toBe(true)
100
116
  })
101
117
 
102
118
  it('should apply ordering', () => {
103
- const {result} = renderHook(() =>
104
- useDocuments({
105
- filter: '_type == "movie"',
106
- orderings: [{field: 'releaseYear', direction: 'desc'}],
107
- }),
119
+ const {result} = renderHook(
120
+ () =>
121
+ useDocuments({
122
+ filter: '_type == "movie"',
123
+ orderings: [{field: 'releaseYear', direction: 'desc'}],
124
+ }),
125
+ {
126
+ wrapper: ({children}) => (
127
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
128
+ {children}
129
+ </ResourceProvider>
130
+ ),
131
+ },
108
132
  )
109
133
 
110
134
  // First item should be the most recent movie (Interstellar, 2014)
@@ -113,7 +137,13 @@ describe('useDocuments', () => {
113
137
 
114
138
  it('should load more data when loadMore is called', () => {
115
139
  const batchSize = 2
116
- const {result} = renderHook(() => useDocuments({batchSize: batchSize}))
140
+ const {result} = renderHook(() => useDocuments({batchSize: batchSize}), {
141
+ wrapper: ({children}) => (
142
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
143
+ {children}
144
+ </ResourceProvider>
145
+ ),
146
+ })
117
147
 
118
148
  expect(result.current.data.length).toBe(batchSize)
119
149
 
@@ -125,7 +155,13 @@ describe('useDocuments', () => {
125
155
  })
126
156
 
127
157
  it('should indicate when there is more data to load', () => {
128
- const {result} = renderHook(() => useDocuments({batchSize: 3}))
158
+ const {result} = renderHook(() => useDocuments({batchSize: 3}), {
159
+ wrapper: ({children}) => (
160
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
161
+ {children}
162
+ </ResourceProvider>
163
+ ),
164
+ })
129
165
  expect(result.current.hasMore).toBe(true)
130
166
  // Load all remaining data
131
167
  act(() => {
@@ -138,6 +174,11 @@ describe('useDocuments', () => {
138
174
  it('should reset limit when filter changes', () => {
139
175
  const {result, rerender} = renderHook((props) => useDocuments(props), {
140
176
  initialProps: {batchSize: 2, filter: ''},
177
+ wrapper: ({children}) => (
178
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
179
+ {children}
180
+ </ResourceProvider>
181
+ ),
141
182
  })
142
183
  // Initially, data length equals pageSize (2)
143
184
  expect(result.current.data.length).toBe(2)
@@ -153,44 +194,23 @@ describe('useDocuments', () => {
153
194
  expect(result.current.data.length).toBe(2)
154
195
  })
155
196
 
156
- it('should add resources to document handles', () => {
157
- const {result} = renderHook(() => useDocuments({}))
158
-
159
- // Check that the first document handle has the projectId and dataset
160
- expect((result.current.data[0].resource as DatasetResource).projectId).toBe('test')
161
- expect((result.current.data[0].resource as DatasetResource).dataset).toBe('test')
162
-
163
- // Verify all document handles have these properties
164
- expect(
165
- result.current.data.every(
166
- (doc) =>
167
- (doc.resource as DatasetResource).projectId === 'test' &&
168
- (doc.resource as DatasetResource).dataset === 'test',
169
- ),
170
- ).toBe(true)
171
- })
172
-
173
- it('should resolve resourceName to the named dataset resource', () => {
174
- const resources = {
175
- default: {projectId: 'test-project', dataset: 'test-dataset'},
176
- secondary: {projectId: 'secondary-project', dataset: 'secondary-dataset'},
177
- }
178
-
179
- const {result} = renderHook(() => useDocuments({resourceName: 'secondary'}), {
197
+ it('should add projectId and dataset to document handles', () => {
198
+ const {result} = renderHook(() => useDocuments({}), {
180
199
  wrapper: ({children}) => (
181
- <ResourceProvider resource={resources.default} fallback={null}>
182
- <ResourcesContext.Provider value={resources}>{children}</ResourcesContext.Provider>
200
+ <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
201
+ {children}
183
202
  </ResourceProvider>
184
203
  ),
185
204
  })
186
205
 
187
- expect((result.current.data[0].resource as DatasetResource).projectId).toBe('secondary-project')
188
- expect((result.current.data[0].resource as DatasetResource).dataset).toBe('secondary-dataset')
206
+ // Check that the first document handle has the projectId and dataset
207
+ expect(result.current.data[0].projectId).toBe('test-project')
208
+ expect(result.current.data[0].dataset).toBe('test-dataset')
209
+
210
+ // Verify all document handles have these properties
189
211
  expect(
190
212
  result.current.data.every(
191
- (doc) =>
192
- (doc.resource as DatasetResource).projectId === 'secondary-project' &&
193
- (doc.resource as DatasetResource).dataset === 'secondary-dataset',
213
+ (doc) => doc.projectId === 'test-project' && doc.dataset === 'test-dataset',
194
214
  ),
195
215
  ).toBe(true)
196
216
  })
@@ -1,10 +1,11 @@
1
- import {createGroqSearchFilter, type QueryOptions} from '@sanity/sdk'
1
+ import {type DocumentHandle, isDatasetResource, type QueryOptions} from '@sanity/sdk'
2
+ import {createGroqSearchFilter, pickProperties} from '@sanity/sdk/_internal'
2
3
  import {type SortOrderingItem} from '@sanity/types'
3
- import {pick} from 'lodash-es'
4
- import {useCallback, useEffect, useMemo, useState} from 'react'
4
+ import {useCallback, useMemo, useState} from 'react'
5
5
 
6
- import {type DocumentHandle, type ResourceHandle} from '../../config/handles'
6
+ import {type ResourceHandle} from '../../config/handles'
7
7
  import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
8
+ import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
8
9
  import {useQuery} from '../query/useQuery'
9
10
 
10
11
  const DEFAULT_BATCH_SIZE = 25
@@ -20,9 +21,7 @@ export interface DocumentsOptions<
20
21
  TDataset extends string = string,
21
22
  TProjectId extends string = string,
22
23
  >
23
- extends
24
- ResourceHandle<TProjectId, TDataset>,
25
- Pick<QueryOptions<TDocumentType, TDataset, TProjectId>, 'params'> {
24
+ extends ResourceHandle<TDataset, TProjectId>, Pick<QueryOptions, 'perspective' | 'params'> {
26
25
  /**
27
26
  * Filter documents by their `_type`. Can be a single type or an array of types.
28
27
  */
@@ -90,16 +89,18 @@ export interface DocumentsResponse<
90
89
  * @returns An object containing the list of document handles, the loading state, the total count of retrieved document handles, and a function to load more
91
90
  *
92
91
  * @remarks
93
- * - The returned document handles include resource information from the current Sanity instance
92
+ * - The returned document handles include projectId and dataset information from the current Sanity instance
94
93
  * - This makes them ready to use with document operations and other document hooks
95
- * - The hook automatically uses the correct Sanity instance based on the resource in the options
94
+ * - The hook automatically uses the correct Sanity instance based on the projectId and dataset in the options
96
95
  *
97
96
  * @example Basic infinite list with loading more
98
97
  * ```tsx
99
98
  * import {
100
99
  * useDocuments,
100
+ * createDatasetHandle,
101
+ * type DatasetHandle,
101
102
  * type DocumentHandle,
102
- * type DocumentResource,
103
+ * type SortOrderingItem
103
104
  * } from '@sanity/sdk-react'
104
105
  * import {Suspense} from 'react'
105
106
  *
@@ -115,14 +116,14 @@ export interface DocumentsResponse<
115
116
  *
116
117
  * // Define props for the list component
117
118
  * interface DocumentListProps {
118
- * resource: DocumentResource
119
+ * dataset: DatasetHandle
119
120
  * documentType: string
120
121
  * search?: string
121
122
  * }
122
123
  *
123
- * function DocumentList({resource, documentType, search}: DocumentListProps) {
124
+ * function DocumentList({dataset, documentType, search}: DocumentListProps) {
124
125
  * const { data, hasMore, isPending, loadMore, count } = useDocuments({
125
- * resource,
126
+ * ...dataset,
126
127
  * documentType,
127
128
  * search,
128
129
  * batchSize: 10,
@@ -136,7 +137,7 @@ export interface DocumentsResponse<
136
137
  * {data.map((docHandle) => (
137
138
  * <li key={docHandle.documentId}>
138
139
  * <Suspense fallback="Loading…">
139
- * <MyDocumentComponent doc={docHandle} />
140
+ * <MyDocumentComponent docHandle={docHandle} />
140
141
  * </Suspense>
141
142
  * </li>
142
143
  * ))}
@@ -151,7 +152,8 @@ export interface DocumentsResponse<
151
152
  * }
152
153
  *
153
154
  * // Usage:
154
- * // <DocumentList resource={{projectId: 'p1', dataset: 'production'}} documentType="post" search="Sanity" />
155
+ * // const myDatasetHandle = createDatasetHandle({ projectId: 'p1', dataset: 'production' })
156
+ * // <DocumentList dataset={myDatasetHandle} documentType="post" search="Sanity" />
155
157
  * ```
156
158
  *
157
159
  * @example Using `filter` and `params` options for narrowing a collection
@@ -202,8 +204,8 @@ export function useDocuments<
202
204
  TDataset,
203
205
  TProjectId
204
206
  > {
205
- const options =
206
- useNormalizedResourceOptions<DocumentsOptions<TDocumentType, TDataset, TProjectId>>(rawOptions)
207
+ useTrackHookUsage('useDocuments')
208
+ const options = useNormalizedResourceOptions(rawOptions)
207
209
  const [limit, setLimit] = useState(batchSize)
208
210
  const documentTypes = useMemo(
209
211
  () =>
@@ -224,9 +226,11 @@ export function useDocuments<
224
226
  types: documentTypes,
225
227
  ...options,
226
228
  })
227
- useEffect(() => {
229
+ const [prevKey, setPrevKey] = useState(key)
230
+ if (prevKey !== key) {
231
+ setPrevKey(key)
228
232
  setLimit(batchSize)
229
- }, [key, batchSize])
233
+ }
230
234
 
231
235
  const filterClause = useMemo(() => {
232
236
  const conditions: string[] = []
@@ -276,7 +280,14 @@ export function useDocuments<
276
280
  params: {
277
281
  ...params,
278
282
  // these are passed back to the user as part of each document handle
279
- __handle: pick(options, ['resource', 'perspective']),
283
+ __handle: {
284
+ // keep projectId/dataset for backward compat until v4; resource is added
285
+ // intentionally so that hook consumers can resolve the correct resource
286
+ ...(options.resource && isDatasetResource(options.resource)
287
+ ? pickProperties(options.resource, ['projectId', 'dataset'])
288
+ : {}),
289
+ ...pickProperties(options, ['perspective', 'resource']),
290
+ },
280
291
  __types: documentTypes,
281
292
  },
282
293
  })