@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.30
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 +6 -100
- package/dist/index.d.ts +2390 -2
- package/dist/index.js +1119 -2
- package/dist/index.js.map +1 -1
- package/package.json +35 -49
- package/src/_exports/index.ts +2 -10
- package/src/_exports/sdk-react.ts +73 -0
- package/src/components/SDKProvider.test.tsx +103 -0
- package/src/components/SDKProvider.tsx +52 -0
- package/src/components/SanityApp.test.tsx +244 -0
- package/src/components/SanityApp.tsx +106 -0
- package/src/components/auth/AuthBoundary.test.tsx +204 -29
- package/src/components/auth/AuthBoundary.tsx +96 -19
- package/src/components/auth/ConfigurationError.ts +22 -0
- package/src/components/auth/LoginCallback.test.tsx +22 -24
- package/src/components/auth/LoginCallback.tsx +6 -16
- package/src/components/auth/LoginError.test.tsx +11 -18
- package/src/components/auth/LoginError.tsx +43 -25
- package/src/components/utils.ts +22 -0
- package/src/context/ResourceProvider.test.tsx +157 -0
- package/src/context/ResourceProvider.tsx +111 -0
- package/src/context/SanityInstanceContext.ts +4 -0
- package/src/hooks/_synchronous-groq-js.mjs +4 -0
- package/src/hooks/auth/useAuthState.tsx +4 -5
- package/src/hooks/auth/useAuthToken.tsx +1 -1
- package/src/hooks/auth/useCurrentUser.tsx +28 -4
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
- package/src/hooks/auth/useDashboardOrganizationId.tsx +30 -0
- package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
- package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
- package/src/hooks/auth/useLogOut.test.tsx +2 -2
- package/src/hooks/auth/useLogOut.tsx +1 -1
- package/src/hooks/auth/useLoginUrl.tsx +14 -0
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
- package/src/hooks/client/useClient.ts +13 -33
- package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
- package/src/hooks/comlink/useFrameConnection.ts +107 -0
- package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
- package/src/hooks/comlink/useManageFavorite.ts +210 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
- package/src/hooks/comlink/useWindowConnection.ts +123 -0
- package/src/hooks/context/useSanityInstance.test.tsx +157 -15
- package/src/hooks/context/useSanityInstance.ts +68 -11
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
- package/src/hooks/datasets/useDatasets.test.ts +80 -0
- package/src/hooks/datasets/useDatasets.ts +52 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
- package/src/hooks/document/useApplyDocumentActions.ts +124 -0
- package/src/hooks/document/useDocument.test.ts +118 -0
- package/src/hooks/document/useDocument.ts +212 -0
- package/src/hooks/document/useDocumentEvent.test.ts +62 -0
- package/src/hooks/document/useDocumentEvent.ts +94 -0
- package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
- package/src/hooks/document/useDocumentPermissions.ts +131 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
- package/src/hooks/document/useEditDocument.test.ts +196 -0
- package/src/hooks/document/useEditDocument.ts +314 -0
- package/src/hooks/documents/useDocuments.test.tsx +179 -0
- package/src/hooks/documents/useDocuments.ts +300 -0
- package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
- package/src/hooks/helpers/createCallbackHook.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
- package/src/hooks/preview/usePreview.test.tsx +85 -17
- package/src/hooks/preview/usePreview.tsx +81 -22
- package/src/hooks/projection/useProjection.test.tsx +283 -0
- package/src/hooks/projection/useProjection.ts +232 -0
- package/src/hooks/projects/useProject.test.ts +80 -0
- package/src/hooks/projects/useProject.ts +51 -0
- package/src/hooks/projects/useProjects.test.ts +77 -0
- package/src/hooks/projects/useProjects.ts +45 -0
- package/src/hooks/query/useQuery.test.tsx +188 -0
- package/src/hooks/query/useQuery.ts +193 -0
- package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
- package/src/hooks/releases/useActiveReleases.ts +39 -0
- package/src/hooks/releases/usePerspective.test.tsx +120 -0
- package/src/hooks/releases/usePerspective.ts +49 -0
- package/src/hooks/users/useUsers.test.tsx +330 -0
- package/src/hooks/users/useUsers.ts +120 -0
- package/src/utils/getEnv.ts +21 -0
- package/src/version.ts +8 -0
- package/src/vite-env.d.ts +10 -0
- package/dist/_chunks-es/useLogOut.js +0 -44
- package/dist/_chunks-es/useLogOut.js.map +0 -1
- package/dist/assets/bundle-CcAyERuZ.css +0 -11
- package/dist/components.d.ts +0 -259
- package/dist/components.js +0 -301
- package/dist/components.js.map +0 -1
- package/dist/hooks.d.ts +0 -186
- package/dist/hooks.js +0 -81
- package/dist/hooks.js.map +0 -1
- package/src/_exports/components.ts +0 -13
- package/src/_exports/hooks.ts +0 -9
- package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
- package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
- package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
- package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
- package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
- package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
- package/src/components/Login/LoginLinks.test.tsx +0 -100
- package/src/components/Login/LoginLinks.tsx +0 -73
- package/src/components/auth/Login.test.tsx +0 -41
- package/src/components/auth/Login.tsx +0 -45
- package/src/components/auth/LoginFooter.test.tsx +0 -29
- package/src/components/auth/LoginFooter.tsx +0 -65
- package/src/components/auth/LoginLayout.test.tsx +0 -33
- package/src/components/auth/LoginLayout.tsx +0 -81
- package/src/components/context/SanityProvider.test.tsx +0 -25
- package/src/components/context/SanityProvider.tsx +0 -42
- package/src/css/css.config.js +0 -220
- package/src/css/paramour.css +0 -2347
- package/src/css/styles.css +0 -11
- package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
- package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
- package/src/hooks/auth/useLoginUrls.tsx +0 -51
- package/src/hooks/client/useClient.test.tsx +0 -130
- package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
- package/src/hooks/documentCollection/useDocuments.ts +0 -87
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getQueryKey,
|
|
3
|
+
getQueryState,
|
|
4
|
+
parseQueryKey,
|
|
5
|
+
type QueryOptions,
|
|
6
|
+
resolveQuery,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {type SanityQueryResult} from 'groq'
|
|
9
|
+
import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
|
|
10
|
+
|
|
11
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
|
|
13
|
+
// Overload 1: Inferred Type (using Typegen)
|
|
14
|
+
/**
|
|
15
|
+
* @beta
|
|
16
|
+
* Executes a GROQ query, inferring the result type from the query string and options.
|
|
17
|
+
* Leverages Sanity Typegen if configured for enhanced type safety.
|
|
18
|
+
*
|
|
19
|
+
* @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
|
|
20
|
+
* @returns An object containing `data` (typed based on the query) and `isPending` (for transitions).
|
|
21
|
+
*
|
|
22
|
+
* @example Basic usage (Inferred Type)
|
|
23
|
+
* ```tsx
|
|
24
|
+
* import {useQuery} from '@sanity/sdk-react'
|
|
25
|
+
* import {defineQuery} from 'groq'
|
|
26
|
+
*
|
|
27
|
+
* const myQuery = defineQuery(`*[_type == "movie"]{_id, title}`)
|
|
28
|
+
*
|
|
29
|
+
* function MovieList() {
|
|
30
|
+
* // Typegen infers the return type for data
|
|
31
|
+
* const {data} = useQuery({ query: myQuery })
|
|
32
|
+
*
|
|
33
|
+
* return (
|
|
34
|
+
* <div>
|
|
35
|
+
* <h2>Movies</h2>
|
|
36
|
+
* <ul>
|
|
37
|
+
* {data.map(movie => <li key={movie._id}>{movie.title}</li>)}
|
|
38
|
+
* </ul>
|
|
39
|
+
* </div>
|
|
40
|
+
* )
|
|
41
|
+
* }
|
|
42
|
+
* // Suspense boundary should wrap <MovieList /> for initial load
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example Using parameters (Inferred Type)
|
|
46
|
+
* ```tsx
|
|
47
|
+
* import {useQuery} from '@sanity/sdk-react'
|
|
48
|
+
* import {defineQuery} from 'groq'
|
|
49
|
+
*
|
|
50
|
+
* const myQuery = defineQuery(`*[_type == "movie" && _id == $id][0]`)
|
|
51
|
+
*
|
|
52
|
+
* function MovieDetails({movieId}: {movieId: string}) {
|
|
53
|
+
* // Typegen infers the return type based on query and params
|
|
54
|
+
* const {data, isPending} = useQuery({
|
|
55
|
+
* query: myQuery,
|
|
56
|
+
* params: { id: movieId }
|
|
57
|
+
* })
|
|
58
|
+
*
|
|
59
|
+
* return (
|
|
60
|
+
* // utilize `isPending` to signal to users that new data is coming in
|
|
61
|
+
* // (e.g. the `movieId` changed and we're loading in the new one)
|
|
62
|
+
* <div style={{ opacity: isPending ? 0.5 : 1 }}>
|
|
63
|
+
* {data ? <h1>{data.title}</h1> : <p>Movie not found</p>}
|
|
64
|
+
* </div>
|
|
65
|
+
* )
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function useQuery<
|
|
70
|
+
TQuery extends string = string,
|
|
71
|
+
TDataset extends string = string,
|
|
72
|
+
TProjectId extends string = string,
|
|
73
|
+
>(
|
|
74
|
+
options: QueryOptions<TQuery, TDataset, TProjectId>,
|
|
75
|
+
): {
|
|
76
|
+
/** The query result, typed based on the GROQ query string */
|
|
77
|
+
data: SanityQueryResult<TQuery, TDataset, TProjectId>
|
|
78
|
+
/** True if a query transition is in progress */
|
|
79
|
+
isPending: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Overload 2: Explicit Type Provided
|
|
83
|
+
/**
|
|
84
|
+
* @beta
|
|
85
|
+
* Executes a GROQ query with an explicitly provided result type `TData`.
|
|
86
|
+
*
|
|
87
|
+
* @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
|
|
88
|
+
* @returns An object containing `data` (cast to `TData`) and `isPending` (indicates whether a query resolution is pending; note that Suspense handles initial loading states). *
|
|
89
|
+
* @example Manually typed query result
|
|
90
|
+
* ```tsx
|
|
91
|
+
* import {useQuery} from '@sanity/sdk-react'
|
|
92
|
+
*
|
|
93
|
+
* interface CustomMovieTitle {
|
|
94
|
+
* movieTitle?: string
|
|
95
|
+
* }
|
|
96
|
+
*
|
|
97
|
+
* function FirstMovieTitle() {
|
|
98
|
+
* // Provide the explicit type TData
|
|
99
|
+
* const {data, isPending} = useQuery<CustomMovieTitle>({
|
|
100
|
+
* query: '*[_type == "movie"][0]{ "movieTitle": title }'
|
|
101
|
+
* })
|
|
102
|
+
*
|
|
103
|
+
* return (
|
|
104
|
+
* <h1 style={{ opacity: isPending ? 0.5 : 1 }}>
|
|
105
|
+
* {data?.movieTitle ?? 'No title found'}
|
|
106
|
+
* </h1>
|
|
107
|
+
* )
|
|
108
|
+
* }
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function useQuery<TData>(options: QueryOptions): {
|
|
112
|
+
/** The query result, cast to the provided type TData */
|
|
113
|
+
data: TData
|
|
114
|
+
/** True if another query is resolving in the background (suspense handles the initial loading state) */
|
|
115
|
+
isPending: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @beta
|
|
120
|
+
* Fetches data and subscribes to real-time updates using a GROQ query.
|
|
121
|
+
*
|
|
122
|
+
* @remarks
|
|
123
|
+
* This hook provides a convenient way to fetch data from your Sanity dataset and
|
|
124
|
+
* automatically receive updates in real-time when the queried data changes.
|
|
125
|
+
*
|
|
126
|
+
* Features:
|
|
127
|
+
* - Executes any valid GROQ query.
|
|
128
|
+
* - Subscribes to changes, providing real-time updates.
|
|
129
|
+
* - Integrates with React Suspense for handling initial loading states.
|
|
130
|
+
* - Uses React Transitions for managing loading states during query/parameter changes (indicated by `isPending`).
|
|
131
|
+
* - Supports type inference based on the GROQ query when using Sanity Typegen.
|
|
132
|
+
* - Allows specifying an explicit return type `TData` for the query result.
|
|
133
|
+
*
|
|
134
|
+
* @category GROQ
|
|
135
|
+
*/
|
|
136
|
+
export function useQuery(options: QueryOptions): {data: unknown; isPending: boolean} {
|
|
137
|
+
// Implementation returns unknown, overloads define specifics
|
|
138
|
+
const instance = useSanityInstance(options)
|
|
139
|
+
|
|
140
|
+
// Use React's useTransition to avoid UI jank when queries change
|
|
141
|
+
const [isPending, startTransition] = useTransition()
|
|
142
|
+
|
|
143
|
+
// Get the unique key for this query and its options
|
|
144
|
+
const queryKey = getQueryKey(options)
|
|
145
|
+
// Use a deferred state to avoid immediate re-renders when the query changes
|
|
146
|
+
const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
|
|
147
|
+
// Parse the deferred query key back into a query and options
|
|
148
|
+
const deferred = useMemo(() => parseQueryKey(deferredQueryKey), [deferredQueryKey])
|
|
149
|
+
|
|
150
|
+
// Create an AbortController to cancel in-flight requests when needed
|
|
151
|
+
const ref = useRef<AbortController>(new AbortController())
|
|
152
|
+
|
|
153
|
+
// When the query or options change, start a transition to update the query
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (queryKey === deferredQueryKey) return
|
|
156
|
+
|
|
157
|
+
startTransition(() => {
|
|
158
|
+
// Abort any in-flight requests for the previous query
|
|
159
|
+
if (ref && !ref.current.signal.aborted) {
|
|
160
|
+
ref.current.abort()
|
|
161
|
+
ref.current = new AbortController()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setDeferredQueryKey(queryKey)
|
|
165
|
+
})
|
|
166
|
+
}, [deferredQueryKey, queryKey])
|
|
167
|
+
|
|
168
|
+
// Get the state source for this query from the query store
|
|
169
|
+
const {getCurrent, subscribe} = useMemo(
|
|
170
|
+
() => getQueryState(instance, deferred),
|
|
171
|
+
[instance, deferred],
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
// If data isn't available yet, suspend rendering
|
|
175
|
+
if (getCurrent() === undefined) {
|
|
176
|
+
// Normally, reading from a mutable ref during render can be risky in concurrent mode.
|
|
177
|
+
// However, it is safe here because:
|
|
178
|
+
// 1. React guarantees that while the component is suspended (via throwing a promise),
|
|
179
|
+
// no effects or state updates occur during that render pass.
|
|
180
|
+
// 2. We immediately capture the current abort signal in a local variable (currentSignal).
|
|
181
|
+
// 3. Even if a background render updates ref.current (for example, due to a query change),
|
|
182
|
+
// the captured signal remains unchanged for this suspended render.
|
|
183
|
+
// Thus, the promise thrown here uses a stable abort signal, ensuring correct behavior.
|
|
184
|
+
const currentSignal = ref.current.signal
|
|
185
|
+
// eslint-disable-next-line react-compiler/react-compiler
|
|
186
|
+
throw resolveQuery(instance, {...deferred, signal: currentSignal})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Subscribe to updates and get the current data
|
|
190
|
+
// useSyncExternalStore ensures the component re-renders when the data changes
|
|
191
|
+
const data = useSyncExternalStore(subscribe, getCurrent) as SanityQueryResult
|
|
192
|
+
return useMemo(() => ({data, isPending}), [data, isPending])
|
|
193
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {getActiveReleasesState, type ReleaseDocument, type SanityInstance} 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 {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
+
import {useActiveReleases} from './useActiveReleases'
|
|
8
|
+
|
|
9
|
+
// Mock the useSanityInstance hook
|
|
10
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
11
|
+
useSanityInstance: vi.fn(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
// Mock the getActiveReleasesState function
|
|
15
|
+
vi.mock('@sanity/sdk', async () => {
|
|
16
|
+
const actual = await vi.importActual('@sanity/sdk')
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
getActiveReleasesState: vi.fn(),
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('useActiveReleases', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should suspend when initial state is undefined', () => {
|
|
29
|
+
const mockInstance = {} as SanityInstance
|
|
30
|
+
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
31
|
+
|
|
32
|
+
const mockSubject = new BehaviorSubject<ReleaseDocument[] | undefined>(undefined)
|
|
33
|
+
const mockStateSource = {
|
|
34
|
+
subscribe: vi.fn((callback) => {
|
|
35
|
+
const subscription = mockSubject.subscribe(callback)
|
|
36
|
+
return () => subscription.unsubscribe()
|
|
37
|
+
}),
|
|
38
|
+
getCurrent: vi.fn(() => undefined),
|
|
39
|
+
observable: mockSubject,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
vi.mocked(getActiveReleasesState).mockReturnValue(mockStateSource)
|
|
43
|
+
|
|
44
|
+
const {result} = renderHook(() => {
|
|
45
|
+
try {
|
|
46
|
+
return useActiveReleases()
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return e
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Verify that the hook threw a promise (suspended)
|
|
53
|
+
expect(result.current).toBeInstanceOf(Promise)
|
|
54
|
+
expect(mockStateSource.getCurrent).toHaveBeenCalled()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should resolve with releases when data is available', () => {
|
|
58
|
+
const mockInstance = {} as SanityInstance
|
|
59
|
+
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
60
|
+
|
|
61
|
+
const mockReleases: ReleaseDocument[] = [
|
|
62
|
+
{_id: 'release1', _type: 'release'} as ReleaseDocument,
|
|
63
|
+
{_id: 'release2', _type: 'release'} as ReleaseDocument,
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const mockSubject = new BehaviorSubject<ReleaseDocument[]>(mockReleases)
|
|
67
|
+
const mockStateSource = {
|
|
68
|
+
subscribe: vi.fn((callback) => {
|
|
69
|
+
const subscription = mockSubject.subscribe(callback)
|
|
70
|
+
return () => subscription.unsubscribe()
|
|
71
|
+
}),
|
|
72
|
+
getCurrent: vi.fn(() => mockReleases),
|
|
73
|
+
observable: mockSubject,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
vi.mocked(getActiveReleasesState).mockReturnValue(mockStateSource)
|
|
77
|
+
|
|
78
|
+
const {result} = renderHook(() => useActiveReleases())
|
|
79
|
+
|
|
80
|
+
// Verify that the hook returned the releases without suspending
|
|
81
|
+
expect(result.current).toEqual(mockReleases)
|
|
82
|
+
expect(mockStateSource.getCurrent).toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getActiveReleasesState,
|
|
3
|
+
type ReleaseDocument,
|
|
4
|
+
type SanityInstance,
|
|
5
|
+
type StateSource,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {filter, firstValueFrom} from 'rxjs'
|
|
8
|
+
|
|
9
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
type UseActiveReleases = {
|
|
15
|
+
(): ReleaseDocument[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @public
|
|
20
|
+
|
|
21
|
+
* Returns the active releases for the current project,
|
|
22
|
+
* represented as a list of release documents.
|
|
23
|
+
*
|
|
24
|
+
* @returns The active releases for the current project.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import {useActiveReleases} from '@sanity/sdk-react'
|
|
29
|
+
*
|
|
30
|
+
* const activeReleases = useActiveReleases()
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const useActiveReleases: UseActiveReleases = createStateSourceHook({
|
|
34
|
+
getState: getActiveReleasesState as (instance: SanityInstance) => StateSource<ReleaseDocument[]>,
|
|
35
|
+
shouldSuspend: (instance: SanityInstance) =>
|
|
36
|
+
getActiveReleasesState(instance).getCurrent() === undefined,
|
|
37
|
+
suspender: (instance: SanityInstance) =>
|
|
38
|
+
firstValueFrom(getActiveReleasesState(instance).observable.pipe(filter(Boolean))),
|
|
39
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {type ClientPerspective} from '@sanity/client'
|
|
2
|
+
import {
|
|
3
|
+
getActiveReleasesState,
|
|
4
|
+
getPerspectiveState,
|
|
5
|
+
type PerspectiveHandle,
|
|
6
|
+
type ReleaseDocument,
|
|
7
|
+
type SanityInstance,
|
|
8
|
+
} from '@sanity/sdk'
|
|
9
|
+
import {renderHook} from '@testing-library/react'
|
|
10
|
+
import {BehaviorSubject} from 'rxjs'
|
|
11
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
12
|
+
|
|
13
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
14
|
+
import {usePerspective} from './usePerspective'
|
|
15
|
+
|
|
16
|
+
// Mock the useSanityInstance hook
|
|
17
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
18
|
+
useSanityInstance: vi.fn(),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
// Mock the SDK functions
|
|
22
|
+
vi.mock('@sanity/sdk', async () => {
|
|
23
|
+
const actual = await vi.importActual('@sanity/sdk')
|
|
24
|
+
return {
|
|
25
|
+
...actual,
|
|
26
|
+
getPerspectiveState: vi.fn(),
|
|
27
|
+
// getPerspectiveState uses getActiveReleasesState
|
|
28
|
+
// to determine if it should suspend
|
|
29
|
+
getActiveReleasesState: vi.fn(),
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('usePerspective', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should suspend when initial state is undefined', () => {
|
|
39
|
+
const mockInstance = {} as SanityInstance
|
|
40
|
+
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
41
|
+
|
|
42
|
+
const perspectiveHandle: PerspectiveHandle = {
|
|
43
|
+
perspective: 'published',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mockSubject = new BehaviorSubject<ClientPerspective | undefined>(undefined)
|
|
47
|
+
const mockStateSource = {
|
|
48
|
+
subscribe: vi.fn((callback) => {
|
|
49
|
+
const subscription = mockSubject.subscribe(callback)
|
|
50
|
+
return () => subscription.unsubscribe()
|
|
51
|
+
}),
|
|
52
|
+
getCurrent: vi.fn(() => undefined),
|
|
53
|
+
observable: mockSubject,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Mock the active releases observable for the suspender
|
|
57
|
+
const mockReleaseDoc: ReleaseDocument = {
|
|
58
|
+
_id: 'release1',
|
|
59
|
+
_type: 'release',
|
|
60
|
+
_createdAt: '2021-01-01T00:00:00Z',
|
|
61
|
+
_updatedAt: '2021-01-01T00:00:00Z',
|
|
62
|
+
_rev: 'rev1',
|
|
63
|
+
name: 'Test Release',
|
|
64
|
+
metadata: {
|
|
65
|
+
title: 'Test Release',
|
|
66
|
+
releaseType: 'asap',
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
const mockReleasesSubject = new BehaviorSubject([mockReleaseDoc])
|
|
70
|
+
const mockReleasesStateSource = {
|
|
71
|
+
subscribe: vi.fn(),
|
|
72
|
+
getCurrent: vi.fn(),
|
|
73
|
+
observable: mockReleasesSubject,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
vi.mocked(getPerspectiveState).mockReturnValue(mockStateSource)
|
|
77
|
+
vi.mocked(getActiveReleasesState).mockReturnValue(mockReleasesStateSource)
|
|
78
|
+
|
|
79
|
+
const {result} = renderHook(() => {
|
|
80
|
+
try {
|
|
81
|
+
return usePerspective(perspectiveHandle)
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return e
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Verify that the hook threw a promise (suspended)
|
|
88
|
+
expect(result.current).toBeInstanceOf(Promise)
|
|
89
|
+
expect(mockStateSource.getCurrent).toHaveBeenCalled()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should resolve with perspective when data is available', () => {
|
|
93
|
+
const mockInstance = {} as SanityInstance
|
|
94
|
+
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
95
|
+
|
|
96
|
+
const perspectiveHandle: PerspectiveHandle = {
|
|
97
|
+
perspective: 'published',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const mockPerspective: ClientPerspective = 'published'
|
|
101
|
+
const mockSubject = new BehaviorSubject<ClientPerspective>(mockPerspective)
|
|
102
|
+
const mockStateSource = {
|
|
103
|
+
subscribe: vi.fn((callback) => {
|
|
104
|
+
const subscription = mockSubject.subscribe(callback)
|
|
105
|
+
return () => subscription.unsubscribe()
|
|
106
|
+
}),
|
|
107
|
+
getCurrent: vi.fn(() => mockPerspective),
|
|
108
|
+
observable: mockSubject,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
vi.mocked(getPerspectiveState).mockReturnValue(mockStateSource)
|
|
112
|
+
|
|
113
|
+
const {result} = renderHook(() => usePerspective(perspectiveHandle))
|
|
114
|
+
|
|
115
|
+
// Verify that the hook returned the perspective without suspending
|
|
116
|
+
expect(result.current).toEqual(mockPerspective)
|
|
117
|
+
expect(mockStateSource.getCurrent).toHaveBeenCalled()
|
|
118
|
+
expect(getPerspectiveState).toHaveBeenCalledWith(mockInstance, perspectiveHandle)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getActiveReleasesState,
|
|
3
|
+
getPerspectiveState,
|
|
4
|
+
type PerspectiveHandle,
|
|
5
|
+
type SanityInstance,
|
|
6
|
+
type StateSource,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {filter, firstValueFrom} from 'rxjs'
|
|
9
|
+
|
|
10
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
type UsePerspective = {
|
|
16
|
+
(perspectiveHandle: PerspectiveHandle): string | string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @public
|
|
21
|
+
*
|
|
22
|
+
* Returns a single or stack of perspectives for the given perspective handle,
|
|
23
|
+
* which can then be used to correctly query the documents
|
|
24
|
+
* via the `perspective` parameter in the client.
|
|
25
|
+
*
|
|
26
|
+
* @param perspectiveHandle - The perspective handle to get the perspective for.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import {usePerspective, useQuery} from '@sanity/sdk-react'
|
|
31
|
+
|
|
32
|
+
* const perspective = usePerspective({perspective: 'rxg1346', projectId: 'abc123', dataset: 'production'})
|
|
33
|
+
* const {data} = useQuery<Movie[]>('*[_type == "movie"]', {
|
|
34
|
+
* perspective: perspective,
|
|
35
|
+
* })
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @returns The perspective for the given perspective handle.
|
|
39
|
+
*/
|
|
40
|
+
export const usePerspective: UsePerspective = createStateSourceHook({
|
|
41
|
+
getState: getPerspectiveState as (
|
|
42
|
+
instance: SanityInstance,
|
|
43
|
+
perspectiveHandle?: PerspectiveHandle,
|
|
44
|
+
) => StateSource<string | string[]>,
|
|
45
|
+
shouldSuspend: (instance: SanityInstance, options?: PerspectiveHandle): boolean =>
|
|
46
|
+
getPerspectiveState(instance, options).getCurrent() === undefined,
|
|
47
|
+
suspender: (instance: SanityInstance, _options?: PerspectiveHandle) =>
|
|
48
|
+
firstValueFrom(getActiveReleasesState(instance).observable.pipe(filter(Boolean))),
|
|
49
|
+
})
|