@sanity/sdk-react 2.7.0 → 2.9.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 (38) hide show
  1. package/dist/index.d.ts +144 -25
  2. package/dist/index.js +248 -139
  3. package/dist/index.js.map +1 -1
  4. package/package.json +5 -5
  5. package/src/_exports/sdk-react.ts +1 -0
  6. package/src/components/SanityApp.tsx +1 -0
  7. package/src/components/auth/AuthBoundary.test.tsx +3 -0
  8. package/src/components/auth/LoginError.test.tsx +5 -0
  9. package/src/components/auth/LoginError.tsx +22 -1
  10. package/src/context/ResourceProvider.test.tsx +7 -1
  11. package/src/context/ResourceProvider.tsx +6 -0
  12. package/src/context/SDKStudioContext.ts +6 -0
  13. package/src/hooks/dashboard/useDispatchIntent.test.ts +2 -0
  14. package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
  15. package/src/hooks/dashboard/useWindowTitle.ts +112 -0
  16. package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
  17. package/src/hooks/document/useApplyDocumentActions.ts +99 -3
  18. package/src/hooks/document/useDocument.ts +22 -6
  19. package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
  20. package/src/hooks/document/useDocumentEvent.ts +10 -3
  21. package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
  22. package/src/hooks/document/useDocumentPermissions.ts +22 -0
  23. package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
  24. package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
  25. package/src/hooks/document/useEditDocument.ts +34 -8
  26. package/src/hooks/documents/useDocuments.ts +2 -0
  27. package/src/hooks/helpers/useNormalizedSourceOptions.ts +50 -28
  28. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  29. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +2 -0
  30. package/src/hooks/presence/usePresence.ts +2 -0
  31. package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
  32. package/src/hooks/preview/useDocumentPreview.tsx +39 -55
  33. package/src/hooks/projection/useDocumentProjection.ts +2 -0
  34. package/src/hooks/query/useQuery.ts +2 -0
  35. package/src/hooks/releases/useActiveReleases.ts +32 -13
  36. package/src/hooks/releases/usePerspective.ts +26 -14
  37. package/src/hooks/users/useUser.ts +2 -0
  38. package/src/hooks/users/useUsers.ts +2 -0
@@ -1,14 +1,25 @@
1
- import {type DocumentHandle, getPreviewState, type PreviewValue, resolvePreview} from '@sanity/sdk'
2
- import {useCallback, useSyncExternalStore} from 'react'
3
- import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
1
+ import {
2
+ type DocumentHandle,
3
+ PREVIEW_PROJECTION,
4
+ type PreviewQueryResult,
5
+ type PreviewValue,
6
+ transformProjectionToPreview,
7
+ } from '@sanity/sdk'
8
+ import {useMemo} from 'react'
4
9
 
5
10
  import {useSanityInstance} from '../context/useSanityInstance'
11
+ import {
12
+ useNormalizedSourceOptions,
13
+ type WithSourceNameSupport,
14
+ } from '../helpers/useNormalizedSourceOptions'
15
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
16
+ import {useDocumentProjection} from '../projection/useDocumentProjection'
6
17
 
7
18
  /**
8
19
  * @public
9
20
  * @category Types
10
21
  */
11
- export interface useDocumentPreviewOptions extends DocumentHandle {
22
+ export interface useDocumentPreviewOptions extends WithSourceNameSupport<DocumentHandle> {
12
23
  /**
13
24
  * Optional ref object to track visibility. When provided, preview resolution
14
25
  * only occurs when the referenced element is visible in the viewport.
@@ -21,7 +32,7 @@ export interface useDocumentPreviewOptions extends DocumentHandle {
21
32
  * @category Types
22
33
  */
23
34
  export interface useDocumentPreviewResults {
24
- /** The results of inferring the documents preview values */
35
+ /** The results of inferring the document's preview values */
25
36
  data: PreviewValue
26
37
  /** True when inferred preview values are being refreshed */
27
38
  isPending: boolean
@@ -31,7 +42,7 @@ export interface useDocumentPreviewResults {
31
42
  * @public
32
43
  *
33
44
  * Attempts to infer preview values of a document (specified via a `DocumentHandle`),
34
- * including the documents `title`, `subtitle`, `media`, and `status`. These values are live and will update in realtime.
45
+ * including the document's `title`, `subtitle`, `media`, and `status`. These values are live and will update in realtime.
35
46
  * To reduce unnecessary network requests for resolving the preview values, an optional `ref` can be passed to the hook so that preview
36
47
  * resolution will only occur if the `ref` is intersecting the current viewport.
37
48
  *
@@ -40,9 +51,13 @@ export interface useDocumentPreviewResults {
40
51
  * @remarks
41
52
  * Values returned by this hook may not be as expected. It is currently unable to read preview values as defined in your schema;
42
53
  * instead, it attempts to infer these preview values by checking against a basic set of potential fields on your document.
43
- * We are anticipating being able to significantly improve this hooks functionality and output in a future release.
54
+ * We are anticipating being able to significantly improve this hook's functionality and output in a future release.
44
55
  * For now, we recommend using {@link useDocumentProjection} for rendering individual document fields (or projections of those fields).
45
56
  *
57
+ * Internally, this hook is implemented as a specialized projection with post-processing logic.
58
+ * It uses a fixed GROQ projection to fetch common preview fields (title, subtitle, media) and
59
+ * transforms the results into the PreviewValue format.
60
+ *
46
61
  * @category Documents
47
62
  * @param options - The document handle for the document you want to infer preview values for, and an optional ref
48
63
  * @returns The inferred values for the given document and a boolean to indicate whether the resolution is pending
@@ -84,56 +99,25 @@ export function useDocumentPreview({
84
99
  ...docHandle
85
100
  }: useDocumentPreviewOptions): useDocumentPreviewResults {
86
101
  const instance = useSanityInstance(docHandle)
87
- const stateSource = getPreviewState(instance, docHandle)
88
-
89
- // Create subscribe function for useSyncExternalStore
90
- const subscribe = useCallback(
91
- (onStoreChanged: () => void) => {
92
- const subscription = new Observable<boolean>((observer) => {
93
- // For environments that don't have an intersection observer (e.g. server-side),
94
- // we pass true to always subscribe since we can't detect visibility
95
- if (typeof IntersectionObserver === 'undefined' || typeof HTMLElement === 'undefined') {
96
- observer.next(true)
97
- return
98
- }
102
+ trackHookUsage(instance, 'useDocumentPreview')
103
+ const normalizedDocHandle = useNormalizedSourceOptions(docHandle)
99
104
 
100
- const intersectionObserver = new IntersectionObserver(
101
- ([entry]) => observer.next(entry.isIntersecting),
102
- {rootMargin: '0px', threshold: 0},
103
- )
104
- if (ref?.current && ref.current instanceof HTMLElement) {
105
- intersectionObserver.observe(ref.current)
106
- } else {
107
- // If no ref is provided or ref.current isn't an HTML element,
108
- // pass true to always subscribe since we can't track visibility
109
- observer.next(true)
110
- }
111
- return () => intersectionObserver.disconnect()
112
- })
113
- .pipe(
114
- startWith(false),
115
- distinctUntilChanged(),
116
- switchMap((isVisible) =>
117
- isVisible
118
- ? new Observable<void>((obs) => {
119
- return stateSource.subscribe(() => obs.next())
120
- })
121
- : EMPTY,
122
- ),
123
- )
124
- .subscribe({next: onStoreChanged})
105
+ // Use the projection hook with the fixed preview projection
106
+ const projectionResult = useDocumentProjection<PreviewQueryResult>({
107
+ ...normalizedDocHandle,
108
+ projection: PREVIEW_PROJECTION,
109
+ ref,
110
+ })
125
111
 
126
- return () => subscription.unsubscribe()
127
- },
128
- [stateSource, ref],
112
+ // Contract: useDocumentProjection suspends while data is null, so data is always available here.
113
+ // Keep this non-null assumption aligned with useDocumentPreviewResults.data.
114
+ const previewValue = useMemo(
115
+ () => transformProjectionToPreview(instance, projectionResult.data, normalizedDocHandle.source),
116
+ [projectionResult.data, instance, normalizedDocHandle.source],
129
117
  )
130
118
 
131
- // Create getSnapshot function to return current state
132
- const getSnapshot = useCallback(() => {
133
- const currentState = stateSource.getCurrent()
134
- if (currentState.data === null) throw resolvePreview(instance, docHandle)
135
- return currentState as useDocumentPreviewResults
136
- }, [docHandle, instance, stateSource])
137
-
138
- return useSyncExternalStore(subscribe, getSnapshot)
119
+ return {
120
+ data: previewValue,
121
+ isPending: projectionResult.isPending,
122
+ }
139
123
  }
@@ -8,6 +8,7 @@ import {
8
8
  useNormalizedSourceOptions,
9
9
  type WithSourceNameSupport,
10
10
  } from '../helpers/useNormalizedSourceOptions'
11
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
11
12
 
12
13
  /**
13
14
  * @public
@@ -181,6 +182,7 @@ export function useDocumentProjection<TData extends object>({
181
182
  ...docHandle
182
183
  }: useDocumentProjectionOptions): useDocumentProjectionResults<TData> {
183
184
  const instance = useSanityInstance(docHandle)
185
+ trackHookUsage(instance, 'useDocumentProjection')
184
186
 
185
187
  // Normalize projection string to handle template literals with whitespace
186
188
  // This ensures that the same projection content produces the same state source
@@ -13,6 +13,7 @@ import {
13
13
  useNormalizedSourceOptions,
14
14
  type WithSourceNameSupport,
15
15
  } from '../helpers/useNormalizedSourceOptions'
16
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
16
17
  /**
17
18
  * Hook options for useQuery, supporting both direct source and sourceName.
18
19
  * @beta
@@ -152,6 +153,7 @@ export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
152
153
  } {
153
154
  // Implementation returns unknown, overloads define specifics
154
155
  const instance = useSanityInstance(options)
156
+ trackHookUsage(instance, 'useQuery')
155
157
 
156
158
  // Normalize options: resolve sourceName to source and strip sourceName
157
159
  const normalized = useNormalizedSourceOptions(options)
@@ -1,19 +1,18 @@
1
1
  import {
2
+ type DocumentSource,
2
3
  getActiveReleasesState,
3
4
  type ReleaseDocument,
5
+ type SanityConfig,
4
6
  type SanityInstance,
5
7
  type StateSource,
6
8
  } from '@sanity/sdk'
7
9
  import {filter, firstValueFrom} from 'rxjs'
8
10
 
9
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
10
-
11
- /**
12
- * @public
13
- */
14
- type UseActiveReleases = {
15
- (): ReleaseDocument[]
16
- }
12
+ import {
13
+ useNormalizedSourceOptions,
14
+ type WithSourceNameSupport,
15
+ } from '../helpers/useNormalizedSourceOptions'
17
16
 
18
17
  /**
19
18
  * @public
@@ -30,10 +29,30 @@ type UseActiveReleases = {
30
29
  * const activeReleases = useActiveReleases()
31
30
  * ```
32
31
  */
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))),
32
+ type UseActiveReleases = {
33
+ (options?: WithSourceNameSupport<SanityConfig> | undefined): ReleaseDocument[]
34
+ }
35
+
36
+ const useActiveReleasesValue: UseActiveReleases = createStateSourceHook({
37
+ getState: getActiveReleasesState as (
38
+ instance: SanityInstance,
39
+ options?: {source?: DocumentSource},
40
+ ) => StateSource<ReleaseDocument[]>,
41
+ shouldSuspend: (instance: SanityInstance, options?: {source?: DocumentSource}) =>
42
+ getActiveReleasesState(instance, options ?? {}).getCurrent() === undefined,
43
+ suspender: (instance: SanityInstance, options?: {source?: DocumentSource}) =>
44
+ firstValueFrom(
45
+ getActiveReleasesState(instance, options ?? {}).observable.pipe(filter(Boolean)),
46
+ ),
39
47
  })
48
+
49
+ /**
50
+ * @public
51
+ * @function
52
+ */
53
+ export const useActiveReleases: UseActiveReleases = (
54
+ options: WithSourceNameSupport<{source?: DocumentSource}> | undefined,
55
+ ) => {
56
+ const normalizedOptions = useNormalizedSourceOptions(options ?? {})
57
+ return useActiveReleasesValue(normalizedOptions)
58
+ }
@@ -1,20 +1,17 @@
1
1
  import {
2
- getActiveReleasesState,
2
+ type DatasetHandle,
3
+ type DocumentSource,
3
4
  getPerspectiveState,
4
- type PerspectiveHandle,
5
5
  type SanityInstance,
6
6
  type StateSource,
7
7
  } from '@sanity/sdk'
8
8
  import {filter, firstValueFrom} from 'rxjs'
9
9
 
10
10
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
11
-
12
- /**
13
- * @public
14
- */
15
- type UsePerspective = {
16
- (perspectiveHandle: PerspectiveHandle): string | string[]
17
- }
11
+ import {
12
+ useNormalizedSourceOptions,
13
+ type WithSourceNameSupport,
14
+ } from '../helpers/useNormalizedSourceOptions'
18
15
 
19
16
  /**
20
17
  * @public
@@ -38,13 +35,28 @@ type UsePerspective = {
38
35
  *
39
36
  * @returns The perspective for the given perspective handle.
40
37
  */
41
- export const usePerspective: UsePerspective = createStateSourceHook({
38
+ type UsePerspective = {
39
+ (perspectiveHandle: DatasetHandle): string | string[]
40
+ }
41
+
42
+ const usePerspectiveValue: UsePerspective = createStateSourceHook({
42
43
  getState: getPerspectiveState as (
43
44
  instance: SanityInstance,
44
- perspectiveHandle?: PerspectiveHandle,
45
+ perspectiveHandle?: {source?: DocumentSource},
45
46
  ) => StateSource<string | string[]>,
46
- shouldSuspend: (instance: SanityInstance, options: PerspectiveHandle): boolean =>
47
+ shouldSuspend: (instance: SanityInstance, options: {source?: DocumentSource}): boolean =>
47
48
  getPerspectiveState(instance, options).getCurrent() === undefined,
48
- suspender: (instance: SanityInstance, _options?: PerspectiveHandle) =>
49
- firstValueFrom(getActiveReleasesState(instance).observable.pipe(filter(Boolean))),
49
+ suspender: (instance: SanityInstance, _options?: {source?: DocumentSource}) =>
50
+ firstValueFrom(getPerspectiveState(instance, _options ?? {}).observable.pipe(filter(Boolean))),
50
51
  })
52
+
53
+ /**
54
+ * @public
55
+ * @function
56
+ */
57
+ export const usePerspective: UsePerspective = (
58
+ options: WithSourceNameSupport<DatasetHandle> | undefined,
59
+ ) => {
60
+ const normalizedOptions = useNormalizedSourceOptions(options ?? {})
61
+ return usePerspectiveValue(normalizedOptions)
62
+ }
@@ -9,6 +9,7 @@ import {
9
9
  import {useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
10
10
 
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
12
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
12
13
 
13
14
  /**
14
15
  * @public
@@ -59,6 +60,7 @@ export interface UserResult {
59
60
  */
60
61
  export function useUser(options: GetUserOptions): UserResult {
61
62
  const instance = useSanityInstance(options)
63
+ trackHookUsage(instance, 'useUser')
62
64
  // Use React's useTransition to avoid UI jank when user options change
63
65
  const [isPending, startTransition] = useTransition()
64
66
 
@@ -10,6 +10,7 @@ import {
10
10
  import {useCallback, useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
11
11
 
12
12
  import {useSanityInstance} from '../context/useSanityInstance'
13
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
13
14
 
14
15
  /**
15
16
  * @public
@@ -69,6 +70,7 @@ export interface UsersResult {
69
70
  */
70
71
  export function useUsers(options?: GetUsersOptions): UsersResult {
71
72
  const instance = useSanityInstance(options)
73
+ trackHookUsage(instance, 'useUsers')
72
74
  // Use React's useTransition to avoid UI jank when user options change
73
75
  const [isPending, startTransition] = useTransition()
74
76