@sanity/sdk-react 2.6.0 → 2.8.0
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 +68 -0
- package/dist/index.d.ts +544 -20
- package/dist/index.js +118 -72
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/_exports/sdk-react.ts +1 -0
- package/src/components/SanityApp.test.tsx +72 -2
- package/src/components/SanityApp.tsx +52 -10
- package/src/components/auth/AuthBoundary.test.tsx +3 -0
- package/src/components/auth/AuthBoundary.tsx +5 -5
- package/src/components/auth/LoginError.test.tsx +5 -0
- package/src/components/auth/LoginError.tsx +22 -1
- package/src/context/ComlinkTokenRefresh.test.tsx +2 -2
- package/src/context/ComlinkTokenRefresh.tsx +3 -2
- package/src/context/SDKStudioContext.test.tsx +126 -0
- package/src/context/SDKStudioContext.ts +65 -0
- package/src/hooks/agent/agentActions.ts +436 -21
- package/src/hooks/dashboard/useDispatchIntent.test.ts +5 -5
- package/src/hooks/dashboard/useDispatchIntent.ts +5 -5
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +2 -3
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +3 -3
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +85 -0
- package/src/hooks/projection/useDocumentProjection.ts +15 -4
- package/src/hooks/query/useQuery.ts +23 -17
- package/src/hooks/context/useSource.tsx +0 -34
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {type DocumentSource} from '@sanity/sdk'
|
|
2
|
+
import {useContext} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SourcesContext} from '../../context/SourcesContext'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adds React hook support (sourceName resolution) to core types.
|
|
8
|
+
* This wrapper allows hooks to accept `sourceName` as a convenience,
|
|
9
|
+
* which is then resolved to a `DocumentSource` at the React layer.
|
|
10
|
+
* For now, we are trying to avoid source name resolution in core --
|
|
11
|
+
* functions having sources explicitly passed will reduce complexity.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam T - The core type to extend (must have optional `source` field)
|
|
14
|
+
* @beta
|
|
15
|
+
*/
|
|
16
|
+
export type WithSourceNameSupport<T extends {source?: DocumentSource}> = T & {
|
|
17
|
+
/**
|
|
18
|
+
* Optional name of a source to resolve from context.
|
|
19
|
+
* If provided, will be resolved to a `DocumentSource` via `SourcesContext`.
|
|
20
|
+
* @beta
|
|
21
|
+
*/
|
|
22
|
+
sourceName?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes hook options by resolving `sourceName` to a `DocumentSource`.
|
|
27
|
+
* This hook ensures that options passed to core layer functions only contain
|
|
28
|
+
* `source` (never `sourceName`), preventing duplicate cache keys and maintaining
|
|
29
|
+
* clean separation between React and core layers.
|
|
30
|
+
*
|
|
31
|
+
* @typeParam T - The options type (must include optional source field)
|
|
32
|
+
* @param options - Hook options that may include `sourceName` and/or `source`
|
|
33
|
+
* @returns Normalized options with `sourceName` removed and `source` resolved
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Resolution priority:
|
|
37
|
+
* 1. If `sourceName` is provided, resolves it via `SourcesContext` and uses that
|
|
38
|
+
* 2. Otherwise, uses the inline `source` if provided
|
|
39
|
+
* 3. If neither is provided, returns options without a source field
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* function useQuery(options: WithSourceNameSupport<QueryOptions>) {
|
|
44
|
+
* const instance = useSanityInstance(options)
|
|
45
|
+
* const normalized = useNormalizedOptions(options)
|
|
46
|
+
* // normalized now has source but never sourceName
|
|
47
|
+
* const queryKey = getQueryKey(normalized)
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @beta
|
|
52
|
+
*/
|
|
53
|
+
export function useNormalizedSourceOptions<
|
|
54
|
+
T extends {source?: DocumentSource; sourceName?: string},
|
|
55
|
+
>(options: T): Omit<T, 'sourceName'> {
|
|
56
|
+
const {sourceName, ...rest} = options
|
|
57
|
+
if (sourceName && Object.hasOwn(options, 'source')) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Source name ${JSON.stringify(sourceName)} and source ${JSON.stringify(options.source)} cannot be used together.`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolve sourceName to source via context
|
|
64
|
+
const sources = useContext(SourcesContext)
|
|
65
|
+
let resolvedSource: DocumentSource | undefined
|
|
66
|
+
|
|
67
|
+
if (options.source) {
|
|
68
|
+
resolvedSource = options.source
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (sourceName && !Object.hasOwn(sources, sourceName)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`There's no source named ${JSON.stringify(sourceName)} in context. Please use <SourceProvider>.`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sourceName && sources[sourceName]) {
|
|
78
|
+
resolvedSource = sources[sourceName]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...rest,
|
|
83
|
+
source: resolvedSource,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -4,6 +4,10 @@ import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
|
4
4
|
import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
|
|
5
5
|
|
|
6
6
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
+
import {
|
|
8
|
+
useNormalizedSourceOptions,
|
|
9
|
+
type WithSourceNameSupport,
|
|
10
|
+
} from '../helpers/useNormalizedSourceOptions'
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* @public
|
|
@@ -14,7 +18,7 @@ export interface useDocumentProjectionOptions<
|
|
|
14
18
|
TDocumentType extends string = string,
|
|
15
19
|
TDataset extends string = string,
|
|
16
20
|
TProjectId extends string = string,
|
|
17
|
-
> extends DocumentHandle<TDocumentType, TDataset, TProjectId
|
|
21
|
+
> extends WithSourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
|
|
18
22
|
/** The GROQ projection string */
|
|
19
23
|
projection: TProjection
|
|
20
24
|
/** Optional parameters for the projection query */
|
|
@@ -183,15 +187,22 @@ export function useDocumentProjection<TData extends object>({
|
|
|
183
187
|
// even if the string reference changes (e.g., from inline template literals)
|
|
184
188
|
const normalizedProjection = useMemo(() => projection.trim(), [projection])
|
|
185
189
|
|
|
190
|
+
// Normalize options: resolve sourceName to source and strip sourceName
|
|
191
|
+
const normalizedDocHandle = useNormalizedSourceOptions(docHandle)
|
|
192
|
+
|
|
186
193
|
// Memoize stateSource based on normalized projection and docHandle properties
|
|
187
194
|
// This prevents creating a new StateSource on every render when projection content is the same
|
|
188
195
|
const stateSource = useMemo(
|
|
189
|
-
() =>
|
|
190
|
-
|
|
196
|
+
() =>
|
|
197
|
+
getProjectionState<TData>(instance, {
|
|
198
|
+
...normalizedDocHandle,
|
|
199
|
+
projection: normalizedProjection,
|
|
200
|
+
}),
|
|
201
|
+
[instance, normalizedDocHandle, normalizedProjection],
|
|
191
202
|
)
|
|
192
203
|
|
|
193
204
|
if (stateSource.getCurrent()?.data === null) {
|
|
194
|
-
throw resolveProjection(instance, {...
|
|
205
|
+
throw resolveProjection(instance, {...normalizedDocHandle, projection: normalizedProjection})
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
// Create subscribe function for useSyncExternalStore
|
|
@@ -9,16 +9,19 @@ import {type SanityQueryResult} from 'groq'
|
|
|
9
9
|
import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
|
|
10
10
|
|
|
11
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
import {
|
|
13
|
+
useNormalizedSourceOptions,
|
|
14
|
+
type WithSourceNameSupport,
|
|
15
|
+
} from '../helpers/useNormalizedSourceOptions'
|
|
16
|
+
/**
|
|
17
|
+
* Hook options for useQuery, supporting both direct source and sourceName.
|
|
18
|
+
* @beta
|
|
19
|
+
*/
|
|
20
|
+
type UseQueryOptions<
|
|
15
21
|
TQuery extends string = string,
|
|
16
22
|
TDataset extends string = string,
|
|
17
23
|
TProjectId extends string = string,
|
|
18
|
-
|
|
19
|
-
> extends QueryOptions<TQuery, TDataset, TProjectId> {
|
|
20
|
-
sourceName?: TSourceName
|
|
21
|
-
}
|
|
24
|
+
> = WithSourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
|
|
22
25
|
|
|
23
26
|
// Overload 1: Inferred Type (using Typegen)
|
|
24
27
|
/**
|
|
@@ -80,9 +83,8 @@ export function useQuery<
|
|
|
80
83
|
TQuery extends string = string,
|
|
81
84
|
TDataset extends string = string,
|
|
82
85
|
TProjectId extends string = string,
|
|
83
|
-
TSourceName extends string = string,
|
|
84
86
|
>(
|
|
85
|
-
options: UseQueryOptions<TQuery, TDataset, TProjectId
|
|
87
|
+
options: UseQueryOptions<TQuery, TDataset, TProjectId>,
|
|
86
88
|
): {
|
|
87
89
|
/** The query result, typed based on the GROQ query string */
|
|
88
90
|
data: SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>
|
|
@@ -119,7 +121,7 @@ export function useQuery<
|
|
|
119
121
|
* }
|
|
120
122
|
* ```
|
|
121
123
|
*/
|
|
122
|
-
export function useQuery<TData>(options: QueryOptions): {
|
|
124
|
+
export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
|
|
123
125
|
/** The query result, cast to the provided type TData */
|
|
124
126
|
data: TData
|
|
125
127
|
/** True if another query is resolving in the background (suspense handles the initial loading state) */
|
|
@@ -144,17 +146,21 @@ export function useQuery<TData>(options: QueryOptions): {
|
|
|
144
146
|
*
|
|
145
147
|
* @category GROQ
|
|
146
148
|
*/
|
|
147
|
-
export function useQuery(options: QueryOptions): {
|
|
149
|
+
export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
|
|
150
|
+
data: unknown
|
|
151
|
+
isPending: boolean
|
|
152
|
+
} {
|
|
148
153
|
// Implementation returns unknown, overloads define specifics
|
|
149
154
|
const instance = useSanityInstance(options)
|
|
150
155
|
|
|
151
|
-
|
|
156
|
+
// Normalize options: resolve sourceName to source and strip sourceName
|
|
157
|
+
const normalized = useNormalizedSourceOptions(options)
|
|
152
158
|
|
|
153
159
|
// Use React's useTransition to avoid UI jank when queries change
|
|
154
160
|
const [isPending, startTransition] = useTransition()
|
|
155
161
|
|
|
156
|
-
// Get the unique key for this query and its options
|
|
157
|
-
const queryKey = getQueryKey(
|
|
162
|
+
// Get the unique key for this query and its options (using normalized options)
|
|
163
|
+
const queryKey = getQueryKey(normalized)
|
|
158
164
|
// Use a deferred state to avoid immediate re-renders when the query changes
|
|
159
165
|
const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
|
|
160
166
|
|
|
@@ -180,8 +186,8 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
|
|
|
180
186
|
// Memoize the options object by depending on the stable string key instead of the parsed object
|
|
181
187
|
const {getCurrent, subscribe} = useMemo(() => {
|
|
182
188
|
const deferred = parseQueryKey(deferredQueryKey)
|
|
183
|
-
return getQueryState(instance,
|
|
184
|
-
}, [instance, deferredQueryKey
|
|
189
|
+
return getQueryState(instance, deferred)
|
|
190
|
+
}, [instance, deferredQueryKey])
|
|
185
191
|
|
|
186
192
|
// If data isn't available yet, suspend rendering
|
|
187
193
|
if (getCurrent() === undefined) {
|
|
@@ -196,7 +202,7 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
|
|
|
196
202
|
const currentSignal = ref.current.signal
|
|
197
203
|
const deferred = parseQueryKey(deferredQueryKey)
|
|
198
204
|
|
|
199
|
-
throw resolveQuery(instance, {...deferred,
|
|
205
|
+
throw resolveQuery(instance, {...deferred, signal: currentSignal})
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
// Subscribe to updates and get the current data
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import {type DatasetHandle, type DocumentHandle, type DocumentSource} from '@sanity/sdk'
|
|
2
|
-
import {useContext} from 'react'
|
|
3
|
-
|
|
4
|
-
import {SourcesContext} from '../../context/SourcesContext'
|
|
5
|
-
|
|
6
|
-
/** Retrieves the named source from context.
|
|
7
|
-
* @beta
|
|
8
|
-
* @param name - The name of the source to retrieve.
|
|
9
|
-
* @returns The source.
|
|
10
|
-
* @example
|
|
11
|
-
* ```tsx
|
|
12
|
-
* const source = useSource('my-source')
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
export function useSource(options: DocumentHandle | DatasetHandle): DocumentSource | undefined {
|
|
16
|
-
const sources = useContext(SourcesContext)
|
|
17
|
-
|
|
18
|
-
// this might return the "default" source in the future once we implement it?
|
|
19
|
-
if (!options.sourceName && !options.source) {
|
|
20
|
-
return undefined
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (options.source) {
|
|
24
|
-
return options.source
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (options.sourceName && !Object.hasOwn(sources, options.sourceName)) {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`There's no source named ${JSON.stringify(options.sourceName)} in context. Please use <SourceProvider>.`,
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return options.sourceName ? sources[options.sourceName] : undefined
|
|
34
|
-
}
|