@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
@@ -5,21 +5,100 @@ import {
5
5
  type QueryOptions,
6
6
  resolveQuery,
7
7
  } from '@sanity/sdk'
8
+ import {type SanityQueryResult} from 'groq'
8
9
  import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
9
10
 
10
- import {type ResourceHandle} from '../../config/handles'
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
- import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
12
+ import {
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
16
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
17
+ /**
18
+ * Hook options for useQuery, supporting both direct resource and resourceName.
19
+ * @beta
20
+ */
21
+ type UseQueryOptions<
22
+ TQuery extends string = string,
23
+ TDataset extends string = string,
24
+ TProjectId extends string = string,
25
+ > = WithResourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
13
26
 
14
- /** Options for useQuery: QueryOptions with resource made optional (resolved from context) */
15
- type ReactQueryOptions = Omit<QueryOptions, 'resource' | 'resourceName'> & ResourceHandle
27
+ // Overload 1: Inferred Type (using Typegen)
28
+ /**
29
+ * @public
30
+ * Executes a GROQ query, inferring the result type from the query string and options.
31
+ * Leverages Sanity Typegen if configured for enhanced type safety.
32
+ *
33
+ * @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
34
+ * @returns An object containing `data` (typed based on the query) and `isPending` (for transitions).
35
+ *
36
+ * @example Basic usage (Inferred Type)
37
+ * ```tsx
38
+ * import {useQuery} from '@sanity/sdk-react'
39
+ * import {defineQuery} from 'groq'
40
+ *
41
+ * const myQuery = defineQuery(`*[_type == "movie"]{_id, title}`)
42
+ *
43
+ * function MovieList() {
44
+ * // Typegen infers the return type for data
45
+ * const {data} = useQuery({ query: myQuery })
46
+ *
47
+ * return (
48
+ * <div>
49
+ * <h2>Movies</h2>
50
+ * <ul>
51
+ * {data.map(movie => <li key={movie._id}>{movie.title}</li>)}
52
+ * </ul>
53
+ * </div>
54
+ * )
55
+ * }
56
+ * // Suspense boundary should wrap <MovieList /> for initial load
57
+ * ```
58
+ *
59
+ * @example Using parameters (Inferred Type)
60
+ * ```tsx
61
+ * import {useQuery} from '@sanity/sdk-react'
62
+ * import {defineQuery} from 'groq'
63
+ *
64
+ * const myQuery = defineQuery(`*[_type == "movie" && _id == $id][0]`)
65
+ *
66
+ * function MovieDetails({movieId}: {movieId: string}) {
67
+ * // Typegen infers the return type based on query and params
68
+ * const {data, isPending} = useQuery({
69
+ * query: myQuery,
70
+ * params: { id: movieId }
71
+ * })
72
+ *
73
+ * return (
74
+ * // utilize `isPending` to signal to users that new data is coming in
75
+ * // (e.g. the `movieId` changed and we're loading in the new one)
76
+ * <div style={{ opacity: isPending ? 0.5 : 1 }}>
77
+ * {data ? <h1>{data.title}</h1> : <p>Movie not found</p>}
78
+ * </div>
79
+ * )
80
+ * }
81
+ * ```
82
+ */
83
+ export function useQuery<
84
+ TQuery extends string = string,
85
+ TDataset extends string = string,
86
+ TProjectId extends string = string,
87
+ >(
88
+ options: UseQueryOptions<TQuery, TDataset, TProjectId>,
89
+ ): {
90
+ /** The query result, typed based on the GROQ query string */
91
+ data: SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>
92
+ /** True if a query transition is in progress */
93
+ isPending: boolean
94
+ }
16
95
 
17
- // Overload 1: Explicit Type Provided
96
+ // Overload 2: Explicit Type Provided
18
97
  /**
19
98
  * @public
20
99
  * Executes a GROQ query with an explicitly provided result type `TData`.
21
100
  *
22
- * @param options - Configuration for the query, including `query`, optional `params`, `resource`, etc.
101
+ * @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
23
102
  * @returns An object containing `data` (cast to `TData`) and `isPending` (indicates whether a query resolution is pending; note that Suspense handles initial loading states). *
24
103
  * @example Manually typed query result
25
104
  * ```tsx
@@ -43,7 +122,7 @@ type ReactQueryOptions = Omit<QueryOptions, 'resource' | 'resourceName'> & Resou
43
122
  * }
44
123
  * ```
45
124
  */
46
- export function useQuery<TData>(options: ReactQueryOptions): {
125
+ export function useQuery<TData>(options: WithResourceNameSupport<QueryOptions>): {
47
126
  /** The query result, cast to the provided type TData */
48
127
  data: TData
49
128
  /** True if another query is resolving in the background (suspense handles the initial loading state) */
@@ -63,16 +142,18 @@ export function useQuery<TData>(options: ReactQueryOptions): {
63
142
  * - Subscribes to changes, providing real-time updates.
64
143
  * - Integrates with React Suspense for handling initial loading states.
65
144
  * - Uses React Transitions for managing loading states during query/parameter changes (indicated by `isPending`).
145
+ * - Supports type inference based on the GROQ query when using Sanity Typegen.
66
146
  * - Allows specifying an explicit return type `TData` for the query result.
67
147
  *
68
148
  * @category GROQ
69
149
  */
70
- export function useQuery(options: ReactQueryOptions): {
150
+ export function useQuery(options: WithResourceNameSupport<QueryOptions>): {
71
151
  data: unknown
72
152
  isPending: boolean
73
153
  } {
74
154
  // Implementation returns unknown, overloads define specifics
75
155
  const instance = useSanityInstance()
156
+ trackHookUsage(instance, 'useQuery')
76
157
 
77
158
  // Normalize options: resolve resourceName to resource and strip resourceName
78
159
  const normalized = useNormalizedResourceOptions(options)
@@ -123,11 +204,12 @@ export function useQuery(options: ReactQueryOptions): {
123
204
  const currentSignal = ref.current.signal
124
205
  const deferred = parseQueryKey(deferredQueryKey)
125
206
 
207
+ // eslint-disable-next-line react-hooks/refs -- intentional during suspended render; see comment above.
126
208
  throw resolveQuery(instance, {...deferred, signal: currentSignal})
127
209
  }
128
210
 
129
211
  // Subscribe to updates and get the current data
130
212
  // useSyncExternalStore ensures the component re-renders when the data changes
131
- const data = useSyncExternalStore(subscribe, getCurrent) as unknown
213
+ const data = useSyncExternalStore(subscribe, getCurrent) as SanityQueryResult
132
214
  return useMemo(() => ({data, isPending}), [data, isPending])
133
215
  }
@@ -20,14 +20,14 @@ describe('useActiveReleases', () => {
20
20
  vi.clearAllMocks()
21
21
  })
22
22
 
23
- it('should suspend when initial state is undefined', () => {
23
+ it('suspends until the releases state source emits, then resolves with the data', async () => {
24
24
  const mockSubject = new BehaviorSubject<ReleaseDocument[] | undefined>(undefined)
25
25
  const mockStateSource = {
26
26
  subscribe: vi.fn((callback) => {
27
27
  const subscription = mockSubject.subscribe(callback)
28
28
  return () => subscription.unsubscribe()
29
29
  }),
30
- getCurrent: vi.fn(() => undefined),
30
+ getCurrent: vi.fn(() => mockSubject.getValue()),
31
31
  observable: mockSubject,
32
32
  }
33
33
 
@@ -43,22 +43,28 @@ describe('useActiveReleases', () => {
43
43
  },
44
44
  {
45
45
  wrapper: ({children}) => (
46
- <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
46
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
47
47
  {children}
48
48
  </ResourceProvider>
49
49
  ),
50
50
  },
51
51
  )
52
52
 
53
- // Verify that the hook threw a promise (suspended)
54
53
  expect(result.current).toBeInstanceOf(Promise)
55
54
  expect(mockStateSource.getCurrent).toHaveBeenCalled()
55
+
56
+ const resolved: ReleaseDocument[] = [
57
+ {_id: 'release1', _type: 'release'} as unknown as ReleaseDocument,
58
+ ]
59
+ mockSubject.next(resolved)
60
+
61
+ await expect(result.current).resolves.toEqual(resolved)
56
62
  })
57
63
 
58
64
  it('should resolve with releases when data is available', () => {
59
65
  const mockReleases: ReleaseDocument[] = [
60
- {_id: 'release1', _type: 'release'} as ReleaseDocument,
61
- {_id: 'release2', _type: 'release'} as ReleaseDocument,
66
+ {_id: 'release1', _type: 'release'} as unknown as ReleaseDocument,
67
+ {_id: 'release2', _type: 'release'} as unknown as ReleaseDocument,
62
68
  ]
63
69
 
64
70
  const mockSubject = new BehaviorSubject<ReleaseDocument[]>(mockReleases)
@@ -75,7 +81,7 @@ describe('useActiveReleases', () => {
75
81
 
76
82
  const {result} = renderHook(() => useActiveReleases(), {
77
83
  wrapper: ({children}) => (
78
- <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
84
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
79
85
  {children}
80
86
  </ResourceProvider>
81
87
  ),
@@ -2,23 +2,44 @@ import {
2
2
  type DocumentResource,
3
3
  getActiveReleasesState,
4
4
  type ReleaseDocument,
5
+ type SanityConfig,
5
6
  type SanityInstance,
6
7
  type StateSource,
7
8
  } from '@sanity/sdk'
8
9
  import {filter, firstValueFrom} from 'rxjs'
9
10
 
10
- import {type ResourceHandle} from '../../config/handles'
11
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
- import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
12
+ import {
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
16
+
17
+ type UseActiveReleasesValue = {
18
+ (options?: {resource?: DocumentResource}): ReleaseDocument[]
19
+ }
20
+
21
+ const useActiveReleasesValue: UseActiveReleasesValue = createStateSourceHook({
22
+ getState: getActiveReleasesState as (
23
+ instance: SanityInstance,
24
+ options?: {resource?: DocumentResource},
25
+ ) => StateSource<ReleaseDocument[]>,
26
+ shouldSuspend: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
27
+ getActiveReleasesState(instance, options ?? {}).getCurrent() === undefined,
28
+ suspender: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
29
+ firstValueFrom(
30
+ getActiveReleasesState(instance, options ?? {}).observable.pipe(filter(Boolean)),
31
+ ),
32
+ })
13
33
 
14
34
  /**
15
35
  * @public
16
-
36
+ * @function
37
+ *
17
38
  * Returns the active releases for the current project,
18
39
  * represented as a list of release documents.
19
40
  *
20
41
  * @returns The active releases for the current project.
21
- * @category Projects
42
+ * @category Releases
22
43
  * @example
23
44
  * ```tsx
24
45
  * import {useActiveReleases} from '@sanity/sdk-react'
@@ -26,26 +47,9 @@ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOpti
26
47
  * const activeReleases = useActiveReleases()
27
48
  * ```
28
49
  */
29
- type UseActiveReleases = {
30
- (options?: ResourceHandle | undefined): ReleaseDocument[]
31
- }
32
-
33
- const useActiveReleasesValue = createStateSourceHook({
34
- getState: getActiveReleasesState as (
35
- instance: SanityInstance,
36
- options: {resource: DocumentResource},
37
- ) => StateSource<ReleaseDocument[]>,
38
- shouldSuspend: (instance: SanityInstance, options: {resource: DocumentResource}) =>
39
- getActiveReleasesState(instance, options).getCurrent() === undefined,
40
- suspender: (instance: SanityInstance, options: {resource: DocumentResource}) =>
41
- firstValueFrom(getActiveReleasesState(instance, options).observable.pipe(filter(Boolean))),
42
- })
43
-
44
- /**
45
- * @public
46
- * @function
47
- */
48
- export const useActiveReleases: UseActiveReleases = (options: ResourceHandle | undefined) => {
50
+ export function useActiveReleases(
51
+ options?: WithResourceNameSupport<SanityConfig> | undefined,
52
+ ): ReleaseDocument[] {
49
53
  const normalizedOptions = useNormalizedResourceOptions(options ?? {})
50
54
  return useActiveReleasesValue(normalizedOptions)
51
55
  }
@@ -0,0 +1,93 @@
1
+ import {getAllReleasesState, type ReleaseDocument} from '@sanity/sdk'
2
+ import {renderHook} from '@testing-library/react'
3
+ import {BehaviorSubject} from 'rxjs'
4
+ import {describe, expect, it, vi} from 'vitest'
5
+
6
+ import {ResourceProvider} from '../../context/ResourceProvider'
7
+ import {useAllReleases} from './useAllReleases'
8
+
9
+ vi.mock('@sanity/sdk', async () => {
10
+ const actual = await vi.importActual('@sanity/sdk')
11
+ return {
12
+ ...actual,
13
+ getAllReleasesState: vi.fn(),
14
+ }
15
+ })
16
+
17
+ describe('useAllReleases', () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks()
20
+ })
21
+
22
+ it('suspends until the releases state source emits, then resolves with the data', async () => {
23
+ const mockSubject = new BehaviorSubject<ReleaseDocument[] | undefined>(undefined)
24
+ const mockStateSource = {
25
+ subscribe: vi.fn((callback) => {
26
+ const subscription = mockSubject.subscribe(callback)
27
+ return () => subscription.unsubscribe()
28
+ }),
29
+ getCurrent: vi.fn(() => mockSubject.getValue()),
30
+ observable: mockSubject,
31
+ }
32
+
33
+ vi.mocked(getAllReleasesState).mockReturnValue(mockStateSource)
34
+
35
+ const {result} = renderHook(
36
+ () => {
37
+ try {
38
+ return useAllReleases()
39
+ } catch (e) {
40
+ return e
41
+ }
42
+ },
43
+ {
44
+ wrapper: ({children}) => (
45
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
46
+ {children}
47
+ </ResourceProvider>
48
+ ),
49
+ },
50
+ )
51
+
52
+ expect(result.current).toBeInstanceOf(Promise)
53
+ expect(mockStateSource.getCurrent).toHaveBeenCalled()
54
+
55
+ const resolved: ReleaseDocument[] = [
56
+ {_id: 'r-active', _type: 'system.release', state: 'active'} as ReleaseDocument,
57
+ ]
58
+ mockSubject.next(resolved)
59
+
60
+ await expect(result.current).resolves.toEqual(resolved)
61
+ })
62
+
63
+ it('returns every release including archived and published once loaded', () => {
64
+ const mockReleases: ReleaseDocument[] = [
65
+ {_id: 'r-active', _type: 'system.release', state: 'active'} as ReleaseDocument,
66
+ {_id: 'r-archived', _type: 'system.release', state: 'archived'} as ReleaseDocument,
67
+ {_id: 'r-published', _type: 'system.release', state: 'published'} as ReleaseDocument,
68
+ ]
69
+
70
+ const mockSubject = new BehaviorSubject<ReleaseDocument[]>(mockReleases)
71
+ const mockStateSource = {
72
+ subscribe: vi.fn((callback) => {
73
+ const subscription = mockSubject.subscribe(callback)
74
+ return () => subscription.unsubscribe()
75
+ }),
76
+ getCurrent: vi.fn(() => mockReleases),
77
+ observable: mockSubject,
78
+ }
79
+
80
+ vi.mocked(getAllReleasesState).mockReturnValue(mockStateSource)
81
+
82
+ const {result} = renderHook(() => useAllReleases(), {
83
+ wrapper: ({children}) => (
84
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
85
+ {children}
86
+ </ResourceProvider>
87
+ ),
88
+ })
89
+
90
+ expect(result.current).toEqual(mockReleases)
91
+ expect(mockStateSource.getCurrent).toHaveBeenCalled()
92
+ })
93
+ })
@@ -0,0 +1,62 @@
1
+ import {
2
+ type DocumentResource,
3
+ getAllReleasesState,
4
+ type ReleaseDocument,
5
+ type SanityConfig,
6
+ type SanityInstance,
7
+ type StateSource,
8
+ } from '@sanity/sdk'
9
+ import {filter, firstValueFrom} from 'rxjs'
10
+
11
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
+ import {
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
16
+
17
+ type UseAllReleasesValue = {
18
+ (options?: {resource?: DocumentResource}): ReleaseDocument[]
19
+ }
20
+
21
+ const useAllReleasesValue: UseAllReleasesValue = createStateSourceHook({
22
+ getState: getAllReleasesState as (
23
+ instance: SanityInstance,
24
+ options?: {resource?: DocumentResource},
25
+ ) => StateSource<ReleaseDocument[]>,
26
+ shouldSuspend: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
27
+ getAllReleasesState(instance, options ?? {}).getCurrent() === undefined,
28
+ suspender: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
29
+ firstValueFrom(getAllReleasesState(instance, options ?? {}).observable.pipe(filter(Boolean))),
30
+ })
31
+
32
+ /**
33
+ * @public
34
+ * @function
35
+ *
36
+ * Returns every release the dataset has — including `archived`, `published`,
37
+ * and mid-transition states (`archiving`, `unarchiving`, `publishing`,
38
+ * `scheduling`).
39
+ *
40
+ * Use this hook when you're building a release-management UI (listing
41
+ * releases, surfacing lifecycle controls, etc.) so a release stays visible
42
+ * across its full lifecycle — including after it's been published or
43
+ * archived. For perspective / content queries, prefer
44
+ * {@link useActiveReleases}, which filters to releases that still affect
45
+ * what's queryable.
46
+ *
47
+ * @returns Every release for the current project, sorted to match the order
48
+ * used by {@link useActiveReleases}.
49
+ * @category Releases
50
+ * @example
51
+ * ```tsx
52
+ * import {useAllReleases} from '@sanity/sdk-react'
53
+ *
54
+ * const releases = useAllReleases()
55
+ * ```
56
+ */
57
+ export function useAllReleases(
58
+ options?: WithResourceNameSupport<SanityConfig> | undefined,
59
+ ): ReleaseDocument[] {
60
+ const normalizedOptions = useNormalizedResourceOptions(options ?? {})
61
+ return useAllReleasesValue(normalizedOptions)
62
+ }
@@ -0,0 +1,66 @@
1
+ import {applyDocumentActions, createSanityInstance} from '@sanity/sdk'
2
+ import {describe, it} from 'vitest'
3
+
4
+ import {renderHook} from '../../../test/test-utils'
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {useApplyReleaseActions} from './useApplyReleaseActions'
7
+
8
+ // Resource resolution, mismatch detection, and context fallback are covered
9
+ // by hooks/helpers/useApplyActions.test.tsx — both this hook and
10
+ // useApplyDocumentActions are typed wrappers over that shared implementation.
11
+ // These tests just verify the wrapper forwards release actions through and
12
+ // supports batching them in a single transaction.
13
+
14
+ vi.mock('@sanity/sdk', async (importOriginal) => {
15
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
16
+ return {...original, applyDocumentActions: vi.fn()}
17
+ })
18
+
19
+ vi.mock('../context/useSanityInstance')
20
+
21
+ const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
22
+
23
+ describe('useApplyReleaseActions', () => {
24
+ beforeEach(() => {
25
+ vi.resetAllMocks()
26
+ vi.mocked(useSanityInstance).mockReturnValueOnce(instance)
27
+ })
28
+
29
+ it('forwards a release action to applyDocumentActions with the resolved resource', () => {
30
+ const {result} = renderHook(() => useApplyReleaseActions())
31
+ result.current({type: 'release.create', releaseId: 'r1', metadata: {releaseType: 'asap'}})
32
+
33
+ expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
34
+ actions: [
35
+ {
36
+ type: 'release.create',
37
+ releaseId: 'r1',
38
+ metadata: {releaseType: 'asap'},
39
+ resource: {projectId: 'test', dataset: 'test'},
40
+ },
41
+ ],
42
+ resource: {projectId: 'test', dataset: 'test'},
43
+ })
44
+ })
45
+
46
+ it('forwards an array of release actions as a single transaction', () => {
47
+ const {result} = renderHook(() => useApplyReleaseActions())
48
+ result.current([
49
+ {type: 'release.create', releaseId: 'r1', metadata: {releaseType: 'asap'}},
50
+ {type: 'release.publish', releaseId: 'r1'},
51
+ ])
52
+
53
+ expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
54
+ actions: [
55
+ {
56
+ type: 'release.create',
57
+ releaseId: 'r1',
58
+ metadata: {releaseType: 'asap'},
59
+ resource: {projectId: 'test', dataset: 'test'},
60
+ },
61
+ {type: 'release.publish', releaseId: 'r1', resource: {projectId: 'test', dataset: 'test'}},
62
+ ],
63
+ resource: {projectId: 'test', dataset: 'test'},
64
+ })
65
+ })
66
+ })
@@ -0,0 +1,82 @@
1
+ import {type ActionsResult, type ReleaseAction} from '@sanity/sdk'
2
+
3
+ import {type ResourceHandle} from '../../config/handles'
4
+ import {useApplyActions} from '../helpers/useApplyActions'
5
+
6
+ /**
7
+ * @public
8
+ */
9
+ interface UseApplyReleaseActions {
10
+ (): (action: ReleaseAction | ReleaseAction[], options?: ResourceHandle) => Promise<ActionsResult>
11
+ }
12
+
13
+ /**
14
+ * @public
15
+ *
16
+ * Provides a stable callback function for applying one or more release actions.
17
+ *
18
+ * This hook wraps the core `applyDocumentActions` functionality from `@sanity/sdk`,
19
+ * integrating it with the React component lifecycle and {@link SanityInstance}.
20
+ * It accepts release-lifecycle actions generated by {@link createRelease},
21
+ * {@link editRelease}, {@link publishRelease}, {@link scheduleRelease},
22
+ * {@link unscheduleRelease}, {@link archiveRelease}, {@link unarchiveRelease},
23
+ * and {@link deleteRelease}.
24
+ *
25
+ * Note that actions submitted via this hook will cascade to the documents in the release.
26
+ * For example, if you create a release and then publish it, the documents in the release will be published.
27
+ * If you delete a published release, the version documents will be deleted.
28
+ *
29
+ * Features:
30
+ * - Applies one or multiple `ReleaseAction` objects.
31
+ * - Supports optimistic updates for create/edit/delete: local release state
32
+ * reflects changes immediately while in-flight.
33
+ * - Handles batching: multiple actions passed together are submitted as a
34
+ * single atomic transaction.
35
+ *
36
+ * Release actions cannot be combined with `liveEdit` document actions in the
37
+ * same transaction. Submit them as separate transactions if you need both.
38
+ *
39
+ * @category Releases
40
+ * @returns A stable callback. When called with a single `ReleaseAction` or an
41
+ * array of `ReleaseAction`s, it returns a promise that resolves to an
42
+ * {@link ActionsResult}.
43
+ *
44
+ * @example Create and schedule a release
45
+ * ```tsx
46
+ * import {
47
+ * createRelease,
48
+ * scheduleRelease,
49
+ * useApplyReleaseActions,
50
+ * type ReleaseHandle,
51
+ * } from '@sanity/sdk-react'
52
+ *
53
+ * function ScheduleReleaseButton({release}: {release: ReleaseHandle}) {
54
+ * const applyRelease = useApplyReleaseActions()
55
+ *
56
+ * const handleSchedule = () =>
57
+ * applyRelease([
58
+ * createRelease(release, {title: 'Summer drop', releaseType: 'asap'}),
59
+ * scheduleRelease(release, '2026-06-01T00:00:00Z'),
60
+ * ])
61
+ *
62
+ * return <button onClick={handleSchedule}>Schedule</button>
63
+ * }
64
+ * ```
65
+ *
66
+ * @example Publish a release
67
+ * ```tsx
68
+ * import {publishRelease, useApplyReleaseActions} from '@sanity/sdk-react'
69
+ *
70
+ * function PublishButton({releaseId}: {releaseId: string}) {
71
+ * const applyRelease = useApplyReleaseActions()
72
+ * return (
73
+ * <button onClick={() => applyRelease(publishRelease({releaseId}))}>
74
+ * Publish all documents in the release
75
+ * </button>
76
+ * )
77
+ * }
78
+ * ```
79
+ */
80
+ export const useApplyReleaseActions: UseApplyReleaseActions = () => {
81
+ return useApplyActions() as ReturnType<UseApplyReleaseActions>
82
+ }
@@ -5,10 +5,11 @@ import {
5
5
  type PerspectiveHandle,
6
6
  type ReleaseDocument,
7
7
  } from '@sanity/sdk'
8
+ import {renderHook} from '@testing-library/react'
8
9
  import {BehaviorSubject} from 'rxjs'
9
10
  import {describe, expect, it, vi} from 'vitest'
10
11
 
11
- import {renderHook} from '../../../test/test-utils'
12
+ import {ResourceProvider} from '../../context/ResourceProvider'
12
13
  import {usePerspective} from './usePerspective'
13
14
 
14
15
  // Mock the SDK functions
@@ -46,7 +47,7 @@ describe('usePerspective', () => {
46
47
  // Mock the active releases observable for the suspender
47
48
  const mockReleaseDoc: ReleaseDocument = {
48
49
  _id: 'release1',
49
- _type: 'release',
50
+ _type: 'system.release',
50
51
  _createdAt: '2021-01-01T00:00:00Z',
51
52
  _updatedAt: '2021-01-01T00:00:00Z',
52
53
  _rev: 'rev1',
@@ -67,13 +68,18 @@ describe('usePerspective', () => {
67
68
  vi.mocked(getPerspectiveState).mockReturnValue(mockStateSource)
68
69
  vi.mocked(getActiveReleasesState).mockReturnValue(mockReleasesStateSource)
69
70
 
70
- const {result} = renderHook(() => {
71
- try {
72
- return usePerspective(perspectiveHandle)
73
- } catch (e) {
74
- return e
75
- }
76
- })
71
+ const {result} = renderHook(
72
+ () => {
73
+ try {
74
+ return usePerspective(perspectiveHandle)
75
+ } catch (e) {
76
+ return e
77
+ }
78
+ },
79
+ {
80
+ wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
81
+ },
82
+ )
77
83
 
78
84
  // Verify that the hook threw a promise (suspended)
79
85
  expect(result.current).toBeInstanceOf(Promise)
@@ -98,7 +104,9 @@ describe('usePerspective', () => {
98
104
 
99
105
  vi.mocked(getPerspectiveState).mockReturnValue(mockStateSource)
100
106
 
101
- const {result} = renderHook(() => usePerspective(perspectiveHandle))
107
+ const {result} = renderHook(() => usePerspective(perspectiveHandle), {
108
+ wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
109
+ })
102
110
 
103
111
  // Verify that the hook returned the perspective without suspending
104
112
  expect(result.current).toEqual(mockPerspective)