@sanity/sdk-react 2.9.0 → 2.10.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.
Files changed (44) hide show
  1. package/dist/index.d.ts +92 -26
  2. package/dist/index.js +304 -193
  3. package/dist/index.js.map +1 -1
  4. package/package.json +9 -11
  5. package/src/_exports/sdk-react.ts +4 -0
  6. package/src/components/SDKProvider.tsx +36 -8
  7. package/src/components/SanityApp.tsx +2 -2
  8. package/src/components/auth/AuthBoundary.tsx +8 -1
  9. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  10. package/src/components/auth/LoginError.test.tsx +191 -5
  11. package/src/components/auth/LoginError.tsx +100 -56
  12. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  13. package/src/components/errors/ChunkLoadError.tsx +56 -0
  14. package/src/components/errors/chunkReloadStorage.ts +57 -0
  15. package/src/context/ResourceProvider.tsx +5 -4
  16. package/src/context/ResourcesContext.tsx +7 -0
  17. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  18. package/src/context/SanityInstanceProvider.tsx +71 -0
  19. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  20. package/src/hooks/dashboard/useDispatchIntent.test.ts +6 -6
  21. package/src/hooks/dashboard/useDispatchIntent.ts +6 -6
  22. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
  23. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -13
  24. package/src/hooks/document/useApplyDocumentActions.test.ts +10 -10
  25. package/src/hooks/document/useApplyDocumentActions.ts +17 -17
  26. package/src/hooks/document/useDocument.ts +5 -5
  27. package/src/hooks/document/useDocumentEvent.ts +4 -4
  28. package/src/hooks/document/useDocumentPermissions.test.tsx +10 -10
  29. package/src/hooks/document/useDocumentPermissions.ts +8 -8
  30. package/src/hooks/document/useDocumentSyncStatus.ts +2 -2
  31. package/src/hooks/document/useEditDocument.ts +2 -2
  32. package/src/hooks/documents/useDocuments.ts +9 -6
  33. package/src/hooks/helpers/useNormalizedResourceOptions.ts +131 -0
  34. package/src/hooks/helpers/useTrackHookUsage.ts +2 -2
  35. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +9 -8
  36. package/src/hooks/presence/usePresence.test.tsx +56 -9
  37. package/src/hooks/presence/usePresence.ts +23 -4
  38. package/src/hooks/preview/useDocumentPreview.tsx +8 -7
  39. package/src/hooks/projection/useDocumentProjection.ts +6 -6
  40. package/src/hooks/query/useQuery.ts +10 -9
  41. package/src/hooks/releases/useActiveReleases.ts +10 -10
  42. package/src/hooks/releases/usePerspective.ts +9 -9
  43. package/src/context/SourcesContext.tsx +0 -7
  44. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -107
@@ -1,19 +1,38 @@
1
- import {getPresence, type UserPresence} from '@sanity/sdk'
1
+ import {
2
+ type DatasetHandle,
3
+ getPresence,
4
+ isMediaLibraryResource,
5
+ type UserPresence,
6
+ } from '@sanity/sdk'
2
7
  import {useCallback, useMemo, useSyncExternalStore} from 'react'
3
8
 
4
9
  import {useSanityInstance} from '../context/useSanityInstance'
10
+ import {
11
+ useNormalizedResourceOptions,
12
+ type WithResourceNameSupport,
13
+ } from '../helpers/useNormalizedResourceOptions'
5
14
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
6
15
 
7
16
  /**
8
- * A hook for subscribing to presence information for the current project.
17
+ * A hook for subscribing to presence information for the current project or Canvas.
9
18
  * @public
10
19
  */
11
- export function usePresence(): {
20
+ export function usePresence(options: WithResourceNameSupport<DatasetHandle> = {}): {
12
21
  locations: UserPresence[]
13
22
  } {
23
+ const normalizedOptions = useNormalizedResourceOptions(options)
24
+ if (normalizedOptions.resource && isMediaLibraryResource(normalizedOptions.resource)) {
25
+ throw new Error(
26
+ 'usePresence() does not support media library resources. Presence tracking requires a canvas or dataset resource.',
27
+ )
28
+ }
29
+
14
30
  const sanityInstance = useSanityInstance()
15
31
  trackHookUsage(sanityInstance, 'usePresence')
16
- const source = useMemo(() => getPresence(sanityInstance), [sanityInstance])
32
+ const source = useMemo(
33
+ () => getPresence(sanityInstance, normalizedOptions),
34
+ [sanityInstance, normalizedOptions],
35
+ )
17
36
  const subscribe = useCallback((callback: () => void) => source.subscribe(callback), [source])
18
37
  const locations = useSyncExternalStore(
19
38
  subscribe,
@@ -9,9 +9,9 @@ import {useMemo} from 'react'
9
9
 
10
10
  import {useSanityInstance} from '../context/useSanityInstance'
11
11
  import {
12
- useNormalizedSourceOptions,
13
- type WithSourceNameSupport,
14
- } from '../helpers/useNormalizedSourceOptions'
12
+ useNormalizedResourceOptions,
13
+ type WithResourceNameSupport,
14
+ } from '../helpers/useNormalizedResourceOptions'
15
15
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
16
16
  import {useDocumentProjection} from '../projection/useDocumentProjection'
17
17
 
@@ -19,7 +19,7 @@ import {useDocumentProjection} from '../projection/useDocumentProjection'
19
19
  * @public
20
20
  * @category Types
21
21
  */
22
- export interface useDocumentPreviewOptions extends WithSourceNameSupport<DocumentHandle> {
22
+ export interface useDocumentPreviewOptions extends WithResourceNameSupport<DocumentHandle> {
23
23
  /**
24
24
  * Optional ref object to track visibility. When provided, preview resolution
25
25
  * only occurs when the referenced element is visible in the viewport.
@@ -100,7 +100,7 @@ export function useDocumentPreview({
100
100
  }: useDocumentPreviewOptions): useDocumentPreviewResults {
101
101
  const instance = useSanityInstance(docHandle)
102
102
  trackHookUsage(instance, 'useDocumentPreview')
103
- const normalizedDocHandle = useNormalizedSourceOptions(docHandle)
103
+ const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
104
104
 
105
105
  // Use the projection hook with the fixed preview projection
106
106
  const projectionResult = useDocumentProjection<PreviewQueryResult>({
@@ -112,8 +112,9 @@ export function useDocumentPreview({
112
112
  // Contract: useDocumentProjection suspends while data is null, so data is always available here.
113
113
  // Keep this non-null assumption aligned with useDocumentPreviewResults.data.
114
114
  const previewValue = useMemo(
115
- () => transformProjectionToPreview(instance, projectionResult.data, normalizedDocHandle.source),
116
- [projectionResult.data, instance, normalizedDocHandle.source],
115
+ () =>
116
+ transformProjectionToPreview(instance, projectionResult.data, normalizedDocHandle.resource),
117
+ [projectionResult.data, instance, normalizedDocHandle.resource],
117
118
  )
118
119
 
119
120
  return {
@@ -5,9 +5,9 @@ import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxj
5
5
 
6
6
  import {useSanityInstance} from '../context/useSanityInstance'
7
7
  import {
8
- useNormalizedSourceOptions,
9
- type WithSourceNameSupport,
10
- } from '../helpers/useNormalizedSourceOptions'
8
+ useNormalizedResourceOptions,
9
+ type WithResourceNameSupport,
10
+ } from '../helpers/useNormalizedResourceOptions'
11
11
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
12
12
 
13
13
  /**
@@ -19,7 +19,7 @@ export interface useDocumentProjectionOptions<
19
19
  TDocumentType extends string = string,
20
20
  TDataset extends string = string,
21
21
  TProjectId extends string = string,
22
- > extends WithSourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
22
+ > extends WithResourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
23
23
  /** The GROQ projection string */
24
24
  projection: TProjection
25
25
  /** Optional parameters for the projection query */
@@ -189,8 +189,8 @@ export function useDocumentProjection<TData extends object>({
189
189
  // even if the string reference changes (e.g., from inline template literals)
190
190
  const normalizedProjection = useMemo(() => projection.trim(), [projection])
191
191
 
192
- // Normalize options: resolve sourceName to source and strip sourceName
193
- const normalizedDocHandle = useNormalizedSourceOptions(docHandle)
192
+ // Normalize options: resolve resourceName to resource and strip resourceName
193
+ const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
194
194
 
195
195
  // Memoize stateSource based on normalized projection and docHandle properties
196
196
  // This prevents creating a new StateSource on every render when projection content is the same
@@ -10,19 +10,19 @@ import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransitio
10
10
 
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
12
  import {
13
- useNormalizedSourceOptions,
14
- type WithSourceNameSupport,
15
- } from '../helpers/useNormalizedSourceOptions'
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
16
16
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
17
17
  /**
18
- * Hook options for useQuery, supporting both direct source and sourceName.
18
+ * Hook options for useQuery, supporting both direct resource and resourceName.
19
19
  * @beta
20
20
  */
21
21
  type UseQueryOptions<
22
22
  TQuery extends string = string,
23
23
  TDataset extends string = string,
24
24
  TProjectId extends string = string,
25
- > = WithSourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
25
+ > = WithResourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
26
26
 
27
27
  // Overload 1: Inferred Type (using Typegen)
28
28
  /**
@@ -122,7 +122,7 @@ export function useQuery<
122
122
  * }
123
123
  * ```
124
124
  */
125
- export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
125
+ export function useQuery<TData>(options: WithResourceNameSupport<QueryOptions>): {
126
126
  /** The query result, cast to the provided type TData */
127
127
  data: TData
128
128
  /** True if another query is resolving in the background (suspense handles the initial loading state) */
@@ -147,7 +147,7 @@ export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
147
147
  *
148
148
  * @category GROQ
149
149
  */
150
- export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
150
+ export function useQuery(options: WithResourceNameSupport<QueryOptions>): {
151
151
  data: unknown
152
152
  isPending: boolean
153
153
  } {
@@ -155,8 +155,8 @@ export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
155
155
  const instance = useSanityInstance(options)
156
156
  trackHookUsage(instance, 'useQuery')
157
157
 
158
- // Normalize options: resolve sourceName to source and strip sourceName
159
- const normalized = useNormalizedSourceOptions(options)
158
+ // Normalize options: resolve resourceName to resource and strip resourceName
159
+ const normalized = useNormalizedResourceOptions(options)
160
160
 
161
161
  // Use React's useTransition to avoid UI jank when queries change
162
162
  const [isPending, startTransition] = useTransition()
@@ -204,6 +204,7 @@ export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
204
204
  const currentSignal = ref.current.signal
205
205
  const deferred = parseQueryKey(deferredQueryKey)
206
206
 
207
+ // eslint-disable-next-line react-hooks/refs -- intentional during suspended render; see comment above.
207
208
  throw resolveQuery(instance, {...deferred, signal: currentSignal})
208
209
  }
209
210
 
@@ -1,5 +1,5 @@
1
1
  import {
2
- type DocumentSource,
2
+ type DocumentResource,
3
3
  getActiveReleasesState,
4
4
  type ReleaseDocument,
5
5
  type SanityConfig,
@@ -10,9 +10,9 @@ import {filter, firstValueFrom} from 'rxjs'
10
10
 
11
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
12
  import {
13
- useNormalizedSourceOptions,
14
- type WithSourceNameSupport,
15
- } from '../helpers/useNormalizedSourceOptions'
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
16
16
 
17
17
  /**
18
18
  * @public
@@ -30,17 +30,17 @@ import {
30
30
  * ```
31
31
  */
32
32
  type UseActiveReleases = {
33
- (options?: WithSourceNameSupport<SanityConfig> | undefined): ReleaseDocument[]
33
+ (options?: WithResourceNameSupport<SanityConfig> | undefined): ReleaseDocument[]
34
34
  }
35
35
 
36
36
  const useActiveReleasesValue: UseActiveReleases = createStateSourceHook({
37
37
  getState: getActiveReleasesState as (
38
38
  instance: SanityInstance,
39
- options?: {source?: DocumentSource},
39
+ options?: {resource?: DocumentResource},
40
40
  ) => StateSource<ReleaseDocument[]>,
41
- shouldSuspend: (instance: SanityInstance, options?: {source?: DocumentSource}) =>
41
+ shouldSuspend: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
42
42
  getActiveReleasesState(instance, options ?? {}).getCurrent() === undefined,
43
- suspender: (instance: SanityInstance, options?: {source?: DocumentSource}) =>
43
+ suspender: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
44
44
  firstValueFrom(
45
45
  getActiveReleasesState(instance, options ?? {}).observable.pipe(filter(Boolean)),
46
46
  ),
@@ -51,8 +51,8 @@ const useActiveReleasesValue: UseActiveReleases = createStateSourceHook({
51
51
  * @function
52
52
  */
53
53
  export const useActiveReleases: UseActiveReleases = (
54
- options: WithSourceNameSupport<{source?: DocumentSource}> | undefined,
54
+ options: WithResourceNameSupport<{resource?: DocumentResource}> | undefined,
55
55
  ) => {
56
- const normalizedOptions = useNormalizedSourceOptions(options ?? {})
56
+ const normalizedOptions = useNormalizedResourceOptions(options ?? {})
57
57
  return useActiveReleasesValue(normalizedOptions)
58
58
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  type DatasetHandle,
3
- type DocumentSource,
3
+ type DocumentResource,
4
4
  getPerspectiveState,
5
5
  type SanityInstance,
6
6
  type StateSource,
@@ -9,9 +9,9 @@ import {filter, firstValueFrom} from 'rxjs'
9
9
 
10
10
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
11
11
  import {
12
- useNormalizedSourceOptions,
13
- type WithSourceNameSupport,
14
- } from '../helpers/useNormalizedSourceOptions'
12
+ useNormalizedResourceOptions,
13
+ type WithResourceNameSupport,
14
+ } from '../helpers/useNormalizedResourceOptions'
15
15
 
16
16
  /**
17
17
  * @public
@@ -42,11 +42,11 @@ type UsePerspective = {
42
42
  const usePerspectiveValue: UsePerspective = createStateSourceHook({
43
43
  getState: getPerspectiveState as (
44
44
  instance: SanityInstance,
45
- perspectiveHandle?: {source?: DocumentSource},
45
+ perspectiveHandle?: {resource?: DocumentResource},
46
46
  ) => StateSource<string | string[]>,
47
- shouldSuspend: (instance: SanityInstance, options: {source?: DocumentSource}): boolean =>
47
+ shouldSuspend: (instance: SanityInstance, options: {resource?: DocumentResource}): boolean =>
48
48
  getPerspectiveState(instance, options).getCurrent() === undefined,
49
- suspender: (instance: SanityInstance, _options?: {source?: DocumentSource}) =>
49
+ suspender: (instance: SanityInstance, _options?: {resource?: DocumentResource}) =>
50
50
  firstValueFrom(getPerspectiveState(instance, _options ?? {}).observable.pipe(filter(Boolean))),
51
51
  })
52
52
 
@@ -55,8 +55,8 @@ const usePerspectiveValue: UsePerspective = createStateSourceHook({
55
55
  * @function
56
56
  */
57
57
  export const usePerspective: UsePerspective = (
58
- options: WithSourceNameSupport<DatasetHandle> | undefined,
58
+ options: WithResourceNameSupport<DatasetHandle> | undefined,
59
59
  ) => {
60
- const normalizedOptions = useNormalizedSourceOptions(options ?? {})
60
+ const normalizedOptions = useNormalizedResourceOptions(options ?? {})
61
61
  return usePerspectiveValue(normalizedOptions)
62
62
  }
@@ -1,7 +0,0 @@
1
- import {type DocumentSource} from '@sanity/sdk'
2
- import {createContext} from 'react'
3
-
4
- /** Context for sources.
5
- * @beta
6
- */
7
- export const SourcesContext = createContext<Record<string, DocumentSource>>({})
@@ -1,107 +0,0 @@
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
- * Pure function that normalizes options by resolving `sourceName` to a `DocumentSource`
27
- * using the provided sources map. Use this when options are only available at call time
28
- * (e.g. inside a callback) and you cannot call the {@link useNormalizedSourceOptions} hook.
29
- *
30
- * @typeParam T - The options type (must include optional source field)
31
- * @param options - Options that may include `sourceName` and/or `source`
32
- * @param sources - Map of source names to DocumentSource (e.g. from SourcesContext)
33
- * @returns Normalized options with `sourceName` removed and `source` resolved
34
- * @internal
35
- */
36
- export function normalizeSourceOptions<T extends {source?: DocumentSource; sourceName?: string}>(
37
- options: T,
38
- sources: Record<string, DocumentSource>,
39
- ): Omit<T, 'sourceName'> {
40
- const {sourceName, ...rest} = options
41
-
42
- if (!sourceName && !options.source) {
43
- return options
44
- }
45
-
46
- if (sourceName && Object.hasOwn(options, 'source')) {
47
- throw new Error(
48
- `Source name ${JSON.stringify(sourceName)} and source ${JSON.stringify(options.source)} cannot be used together.`,
49
- )
50
- }
51
-
52
- let resolvedSource: DocumentSource | undefined
53
-
54
- if (options.source) {
55
- resolvedSource = options.source
56
- }
57
-
58
- if (sourceName && !Object.hasOwn(sources, sourceName)) {
59
- throw new Error(
60
- `There's no source named ${JSON.stringify(sourceName)} in context. Please use <SourceProvider>.`,
61
- )
62
- }
63
-
64
- if (sourceName && sources[sourceName]) {
65
- resolvedSource = sources[sourceName]
66
- }
67
-
68
- return {
69
- ...rest,
70
- source: resolvedSource,
71
- }
72
- }
73
-
74
- /**
75
- * Normalizes hook options by resolving `sourceName` to a `DocumentSource`.
76
- * This hook ensures that options passed to core layer functions only contain
77
- * `source` (never `sourceName`), preventing duplicate cache keys and maintaining
78
- * clean separation between React and core layers.
79
- *
80
- * @typeParam T - The options type (must include optional source field)
81
- * @param options - Hook options that may include `sourceName` and/or `source`
82
- * @returns Normalized options with `sourceName` removed and `source` resolved
83
- *
84
- * @remarks
85
- * Resolution priority:
86
- * 1. If `sourceName` is provided, resolves it via `SourcesContext` and uses that
87
- * 2. Otherwise, uses the inline `source` if provided
88
- * 3. If neither is provided, returns options without a source field
89
- *
90
- * @example
91
- * ```tsx
92
- * function useQuery(options: WithSourceNameSupport<QueryOptions>) {
93
- * const instance = useSanityInstance(options)
94
- * const normalized = useNormalizedOptions(options)
95
- * // normalized now has source but never sourceName
96
- * const queryKey = getQueryKey(normalized)
97
- * }
98
- * ```
99
- *
100
- * @beta
101
- */
102
- export function useNormalizedSourceOptions<
103
- T extends {source?: DocumentSource; sourceName?: string},
104
- >(options: T): Omit<T, 'sourceName'> {
105
- const sources = useContext(SourcesContext)
106
- return normalizeSourceOptions(options, sources)
107
- }