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

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 (160) hide show
  1. package/README.md +103 -155
  2. package/dist/index.d.ts +1564 -682
  3. package/dist/index.js +1118 -612
  4. package/dist/index.js.map +1 -1
  5. package/package.json +51 -54
  6. package/src/_exports/index.ts +2 -4
  7. package/src/_exports/sdk-react.ts +26 -17
  8. package/src/components/SDKProvider.test.tsx +173 -32
  9. package/src/components/SDKProvider.tsx +87 -28
  10. package/src/components/SanityApp.test.tsx +68 -12
  11. package/src/components/SanityApp.tsx +94 -87
  12. package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
  13. package/src/components/auth/AuthBoundary.test.tsx +26 -8
  14. package/src/components/auth/AuthBoundary.tsx +28 -6
  15. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  16. package/src/components/auth/LoginError.test.tsx +191 -5
  17. package/src/components/auth/LoginError.tsx +103 -59
  18. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  19. package/src/components/errors/ChunkLoadError.tsx +56 -0
  20. package/src/components/errors/CorsErrorComponent.tsx +2 -2
  21. package/src/components/errors/chunkReloadStorage.ts +57 -0
  22. package/src/config/handles.ts +29 -27
  23. package/src/constants.ts +5 -0
  24. package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
  25. package/src/context/DefaultResourceContext.ts +3 -3
  26. package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
  27. package/src/context/OrganizationResourcesProvider.tsx +111 -0
  28. package/src/context/PerspectiveContext.ts +3 -3
  29. package/src/context/ProjectContext.ts +15 -0
  30. package/src/context/ResourceProvider.test.tsx +72 -90
  31. package/src/context/ResourceProvider.tsx +59 -93
  32. package/src/context/SDKStudioContext.test.tsx +28 -33
  33. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  34. package/src/context/SanityInstanceProvider.tsx +71 -0
  35. package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
  36. package/src/context/WorkbenchTokenRefresh.tsx +61 -0
  37. package/src/context/renderSanityApp.test.tsx +151 -49
  38. package/src/context/renderSanityApp.tsx +12 -8
  39. package/src/context/workbenchToken.ts +63 -0
  40. package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
  41. package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
  42. package/src/hooks/access/useCheckPermissions.ts +24 -0
  43. package/src/hooks/agent/agentActions.test.tsx +1 -1
  44. package/src/hooks/agent/agentActions.ts +51 -71
  45. package/src/hooks/applications/useApplication.test-d.ts +30 -0
  46. package/src/hooks/applications/useApplication.ts +22 -0
  47. package/src/hooks/applications/useApplications.test-d.ts +31 -0
  48. package/src/hooks/applications/useApplications.ts +25 -0
  49. package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
  50. package/src/hooks/applications/useCreateUserApplication.ts +11 -0
  51. package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
  52. package/src/hooks/applications/useDeleteApplication.ts +11 -0
  53. package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
  54. package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
  55. package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
  56. package/src/hooks/applications/useUpdateApplication.ts +11 -0
  57. package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
  58. package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
  59. package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
  60. package/src/hooks/applications/useUserApplication.ts +14 -0
  61. package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
  62. package/src/hooks/applications/useUserApplications.ts +14 -0
  63. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
  64. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
  65. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  66. package/src/hooks/client/useClient.test.tsx +1 -4
  67. package/src/hooks/client/useClient.ts +1 -0
  68. package/src/hooks/context/useResource.test.tsx +32 -0
  69. package/src/hooks/context/useResource.ts +24 -0
  70. package/src/hooks/context/useSanityInstance.test.tsx +72 -3
  71. package/src/hooks/context/useSanityInstance.ts +30 -8
  72. package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
  73. package/src/hooks/dashboard/useManageFavorite.ts +10 -16
  74. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
  75. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
  76. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
  77. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +1 -2
  78. package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
  79. package/src/hooks/dashboard/useWindowTitle.ts +114 -0
  80. package/src/hooks/datasets/useDatasets.test.tsx +116 -0
  81. package/src/hooks/datasets/useDatasets.ts +33 -13
  82. package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
  83. package/src/hooks/document/useApplyDocumentActions.ts +66 -48
  84. package/src/hooks/document/useCreateDocument.test.tsx +83 -0
  85. package/src/hooks/document/useCreateDocument.ts +117 -0
  86. package/src/hooks/document/useDocument.test.tsx +37 -8
  87. package/src/hooks/document/useDocument.ts +131 -66
  88. package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
  89. package/src/hooks/document/useDocumentEvent.ts +16 -10
  90. package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
  91. package/src/hooks/document/useDocumentPermissions.ts +71 -39
  92. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
  93. package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
  94. package/src/hooks/document/useEditDocument.test.tsx +21 -14
  95. package/src/hooks/document/useEditDocument.ts +163 -17
  96. package/src/hooks/documents/useDocuments.test.tsx +64 -44
  97. package/src/hooks/documents/useDocuments.ts +36 -20
  98. package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
  99. package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
  100. package/src/hooks/helpers/createFetcherHook.ts +69 -0
  101. package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
  102. package/src/hooks/helpers/createMutationHook.tsx +93 -0
  103. package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
  104. package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
  105. package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
  106. package/src/hooks/helpers/useApplyActions.ts +68 -0
  107. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
  108. package/src/hooks/helpers/useNormalizedResourceOptions.ts +92 -50
  109. package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
  110. package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
  111. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  112. package/src/hooks/installations/useInstallation.test-d.ts +19 -0
  113. package/src/hooks/installations/useInstallation.ts +22 -0
  114. package/src/hooks/installations/useInstallations.test-d.ts +26 -0
  115. package/src/hooks/installations/useInstallations.ts +25 -0
  116. package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
  117. package/src/hooks/organizations/useOrganization.test.ts +63 -0
  118. package/src/hooks/organizations/useOrganization.ts +34 -0
  119. package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
  120. package/src/hooks/organizations/useOrganizations.test.ts +63 -0
  121. package/src/hooks/organizations/useOrganizations.ts +39 -0
  122. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
  123. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +40 -22
  124. package/src/hooks/presence/usePresence.test.tsx +0 -15
  125. package/src/hooks/presence/usePresence.ts +9 -20
  126. package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
  127. package/src/hooks/preview/useDocumentPreview.tsx +10 -8
  128. package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
  129. package/src/hooks/projection/useDocumentProjection.ts +81 -2
  130. package/src/hooks/projects/useProject.test-d.ts +56 -0
  131. package/src/hooks/projects/useProject.test.tsx +120 -0
  132. package/src/hooks/projects/useProject.ts +42 -43
  133. package/src/hooks/projects/useProjects.test-d.ts +60 -0
  134. package/src/hooks/projects/useProjects.test.ts +45 -98
  135. package/src/hooks/projects/useProjects.ts +19 -26
  136. package/src/hooks/query/useQuery.test.tsx +6 -5
  137. package/src/hooks/query/useQuery.ts +91 -9
  138. package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
  139. package/src/hooks/releases/useActiveReleases.ts +28 -24
  140. package/src/hooks/releases/useAllReleases.test.tsx +93 -0
  141. package/src/hooks/releases/useAllReleases.ts +62 -0
  142. package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
  143. package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
  144. package/src/hooks/releases/usePerspective.test.tsx +18 -10
  145. package/src/hooks/releases/usePerspective.ts +13 -17
  146. package/src/hooks/users/useUser.test.tsx +3 -9
  147. package/src/hooks/users/useUser.ts +2 -0
  148. package/src/hooks/users/useUsers.test.tsx +103 -7
  149. package/src/hooks/users/useUsers.ts +37 -3
  150. package/src/utils/resolveOrgResources.test.ts +111 -0
  151. package/src/utils/resolveOrgResources.ts +69 -0
  152. package/src/hooks/context/useDefaultResource.test.tsx +0 -25
  153. package/src/hooks/context/useDefaultResource.ts +0 -30
  154. package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
  155. package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
  156. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
  157. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
  158. package/src/hooks/datasets/useDatasets.test.ts +0 -66
  159. package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
  160. package/src/hooks/projects/useProject.test.ts +0 -79
@@ -1,11 +1,19 @@
1
1
  import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
2
+ import {isDeepEqual} from '@sanity/sdk/_internal'
2
3
  import {useCallback, useContext, useMemo, useSyncExternalStore} from 'react'
3
4
  import {filter, firstValueFrom} from 'rxjs'
4
5
 
5
- import {ResourceContext} from '../../context/DefaultResourceContext'
6
6
  import {ResourcesContext} from '../../context/ResourcesContext'
7
7
  import {useSanityInstance} from '../context/useSanityInstance'
8
- import {normalizeResourceOptions} from '../helpers/useNormalizedResourceOptions'
8
+ import {
9
+ normalizeResourceOptions,
10
+ useEffectiveContextResource,
11
+ type WithResourceNameSupport,
12
+ } from '../helpers/useNormalizedResourceOptions'
13
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
14
+
15
+ const noopSubscribe = () => () => {}
16
+ const returnUndefined = () => undefined
9
17
 
10
18
  /**
11
19
  *
@@ -85,63 +93,87 @@ import {normalizeResourceOptions} from '../helpers/useNormalizedResourceOptions'
85
93
  * ```
86
94
  */
87
95
  export function useDocumentPermissions(
88
- actionOrActions: DocumentAction | DocumentAction[],
96
+ actionOrActions:
97
+ | WithResourceNameSupport<DocumentAction>
98
+ | WithResourceNameSupport<DocumentAction>[],
89
99
  ): DocumentPermissionsResult {
90
100
  const instance = useSanityInstance()
91
- const contextResource = useContext(ResourceContext)
101
+ trackHookUsage(instance, 'useDocumentPermissions')
102
+ const effectiveContextResource = useEffectiveContextResource()
92
103
  const resources = useContext(ResourcesContext)
93
104
 
94
- const normalizedActions = useMemo(() => {
95
- return Array.isArray(actionOrActions)
105
+ const {
106
+ actions: normalizedActions,
107
+ resource: actionResource,
108
+ error: validationError,
109
+ } = useMemo(() => {
110
+ const normalized = Array.isArray(actionOrActions)
96
111
  ? actionOrActions.map((action) =>
97
- normalizeResourceOptions(action, resources, contextResource),
112
+ normalizeResourceOptions(action, resources, effectiveContextResource),
98
113
  )
99
- : [normalizeResourceOptions(actionOrActions, resources, contextResource)]
100
- }, [actionOrActions, resources, contextResource])
101
-
102
- // if actions is an array, we need to check that all actions belong to the same resource
103
- let resource
114
+ : [normalizeResourceOptions(actionOrActions, resources, effectiveContextResource)]
104
115
 
105
- for (const action of normalizedActions) {
106
- if (action.resource) {
107
- if (!resource) resource = action.resource
108
- if (action.resource !== resource) {
109
- throw new Error(
110
- `Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
111
- )
116
+ let resource
117
+ for (const action of normalized) {
118
+ if (action.resource) {
119
+ if (!resource) resource = action.resource
120
+ if (!isDeepEqual(action.resource, resource)) {
121
+ return {
122
+ actions: normalized,
123
+ resource,
124
+ error: new Error(
125
+ `Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
126
+ ),
127
+ }
128
+ }
112
129
  }
113
130
  }
114
- }
131
+ return {actions: normalized, resource, error: undefined}
132
+ }, [actionOrActions, resources, effectiveContextResource])
115
133
 
116
- const effectiveResource = resource ?? contextResource
117
-
118
- if (!effectiveResource) {
119
- throw new Error(
120
- 'No resource found. Provide a resource via the action handle or wrap with a resource context.',
121
- )
122
- }
134
+ const effectiveResource = actionResource ?? effectiveContextResource
123
135
 
136
+ // Keep hooks unconditional — validation errors and missing-resource errors are
137
+ // thrown after all hooks so that the hook call count stays stable across renders.
124
138
  const permissionsOptions = useMemo(
125
- () => ({resource: effectiveResource, actions: normalizedActions as DocumentAction[]}),
139
+ () =>
140
+ effectiveResource
141
+ ? {
142
+ resource: effectiveResource,
143
+ // `Omit<>` on `DocumentAction` loses the discriminant; runtime values are still actions.
144
+ actions: normalizedActions as DocumentAction[],
145
+ }
146
+ : undefined,
126
147
  [effectiveResource, normalizedActions],
127
148
  )
128
149
 
129
- const isDocumentReady = useCallback(
130
- () => getPermissionsState(instance, permissionsOptions).getCurrent() !== undefined,
150
+ const stateSource = useMemo(
151
+ () => (permissionsOptions ? getPermissionsState(instance, permissionsOptions) : undefined),
131
152
  [permissionsOptions, instance],
132
153
  )
154
+
155
+ const isDocumentReady = useCallback(
156
+ () => stateSource !== undefined && stateSource.getCurrent() !== undefined,
157
+ [stateSource],
158
+ )
159
+
160
+ const result = useSyncExternalStore(
161
+ stateSource?.subscribe ?? noopSubscribe,
162
+ stateSource?.getCurrent ?? returnUndefined,
163
+ )
164
+
165
+ // All hooks have been called — safe to throw now.
166
+ if (validationError) throw validationError
167
+ if (!effectiveResource) {
168
+ throw new Error(
169
+ 'No resource found. Provide a resource via the action handle or wrap with a resource context.',
170
+ )
171
+ }
133
172
  if (!isDocumentReady()) {
134
173
  throw firstValueFrom(
135
- getPermissionsState(instance, permissionsOptions).observable.pipe(
136
- filter((result) => result !== undefined),
137
- ),
174
+ stateSource!.observable.pipe(filter((permissions) => permissions !== undefined)),
138
175
  )
139
176
  }
140
177
 
141
- const {subscribe, getCurrent} = useMemo(
142
- () => getPermissionsState(instance, permissionsOptions),
143
- [permissionsOptions, instance],
144
- )
145
-
146
- return useSyncExternalStore(subscribe, getCurrent) as DocumentPermissionsResult
178
+ return result as DocumentPermissionsResult
147
179
  }
@@ -25,6 +25,7 @@ describe('useDocumentSyncStatus', () => {
25
25
  getState: getDocumentSyncStatus,
26
26
  shouldSuspend: expect.any(Function),
27
27
  suspender: expect.any(Function),
28
+ getConfig: expect.any(Function),
28
29
  }),
29
30
  )
30
31
  // Verify that the hook was created and can be called
@@ -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()