@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.
- package/README.md +103 -155
- package/dist/index.d.ts +1564 -682
- package/dist/index.js +1118 -612
- package/dist/index.js.map +1 -1
- package/package.json +51 -54
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +26 -17
- package/src/components/SDKProvider.test.tsx +173 -32
- package/src/components/SDKProvider.tsx +87 -28
- package/src/components/SanityApp.test.tsx +68 -12
- package/src/components/SanityApp.tsx +94 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +26 -8
- package/src/components/auth/AuthBoundary.tsx +28 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +103 -59
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/CorsErrorComponent.tsx +2 -2
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +29 -27
- package/src/constants.ts +5 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
- package/src/context/DefaultResourceContext.ts +3 -3
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/PerspectiveContext.ts +3 -3
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +72 -90
- package/src/context/ResourceProvider.tsx +59 -93
- package/src/context/SDKStudioContext.test.tsx +28 -33
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/renderSanityApp.test.tsx +151 -49
- package/src/context/renderSanityApp.tsx +12 -8
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +51 -71
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useCreateUserApplication.ts +11 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
- package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplication.ts +14 -0
- package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplications.ts +14 -0
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/client/useClient.test.tsx +1 -4
- package/src/hooks/client/useClient.ts +1 -0
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +72 -3
- package/src/hooks/context/useSanityInstance.ts +30 -8
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +10 -16
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +1 -2
- package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
- package/src/hooks/dashboard/useWindowTitle.ts +114 -0
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +33 -13
- package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
- package/src/hooks/document/useApplyDocumentActions.ts +66 -48
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.test.tsx +37 -8
- package/src/hooks/document/useDocument.ts +131 -66
- package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
- package/src/hooks/document/useDocumentEvent.ts +16 -10
- package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
- package/src/hooks/document/useDocumentPermissions.ts +71 -39
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
- package/src/hooks/document/useEditDocument.test.tsx +21 -14
- package/src/hooks/document/useEditDocument.ts +163 -17
- package/src/hooks/documents/useDocuments.test.tsx +64 -44
- package/src/hooks/documents/useDocuments.ts +36 -20
- package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
- package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +92 -50
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
- package/src/hooks/organizations/useOrganization.test.ts +63 -0
- package/src/hooks/organizations/useOrganization.ts +34 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
- package/src/hooks/organizations/useOrganizations.test.ts +63 -0
- package/src/hooks/organizations/useOrganizations.ts +39 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +40 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +9 -20
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +10 -8
- package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
- package/src/hooks/projection/useDocumentProjection.ts +81 -2
- package/src/hooks/projects/useProject.test-d.ts +56 -0
- package/src/hooks/projects/useProject.test.tsx +120 -0
- package/src/hooks/projects/useProject.ts +42 -43
- package/src/hooks/projects/useProjects.test-d.ts +60 -0
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +19 -26
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +91 -9
- package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
- package/src/hooks/releases/useActiveReleases.ts +28 -24
- package/src/hooks/releases/useAllReleases.test.tsx +93 -0
- package/src/hooks/releases/useAllReleases.ts +62 -0
- package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
- package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
- package/src/hooks/releases/usePerspective.test.tsx +18 -10
- package/src/hooks/releases/usePerspective.ts +13 -17
- package/src/hooks/users/useUser.test.tsx +3 -9
- package/src/hooks/users/useUser.ts +2 -0
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +37 -3
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/context/useDefaultResource.test.tsx +0 -25
- package/src/hooks/context/useDefaultResource.ts +0 -30
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
- package/src/hooks/datasets/useDatasets.test.ts +0 -66
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
- 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 {
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
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`, `
|
|
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:
|
|
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:
|
|
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
|
|
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('
|
|
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(() =>
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
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 {
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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)
|