@sanity/sdk-react 0.0.0-alpha.9 → 0.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.
Files changed (94) hide show
  1. package/README.md +33 -126
  2. package/dist/index.d.ts +4742 -2
  3. package/dist/index.js +1054 -2
  4. package/dist/index.js.map +1 -1
  5. package/package.json +19 -43
  6. package/src/_exports/index.ts +66 -10
  7. package/src/components/Login/LoginLinks.test.tsx +90 -0
  8. package/src/components/Login/LoginLinks.tsx +58 -0
  9. package/src/components/SDKProvider.test.tsx +79 -0
  10. package/src/components/SDKProvider.tsx +42 -0
  11. package/src/components/SanityApp.test.tsx +104 -2
  12. package/src/components/SanityApp.tsx +54 -17
  13. package/src/components/auth/AuthBoundary.test.tsx +2 -2
  14. package/src/components/auth/AuthBoundary.tsx +13 -3
  15. package/src/components/auth/Login.test.tsx +1 -1
  16. package/src/components/auth/Login.tsx +11 -26
  17. package/src/components/auth/LoginCallback.tsx +4 -7
  18. package/src/components/auth/LoginError.tsx +12 -8
  19. package/src/components/auth/LoginFooter.tsx +13 -20
  20. package/src/components/auth/LoginLayout.tsx +8 -9
  21. package/src/components/auth/authTestHelpers.tsx +1 -8
  22. package/src/components/utils.ts +22 -0
  23. package/src/context/SanityInstanceContext.ts +4 -0
  24. package/src/context/SanityProvider.test.tsx +1 -1
  25. package/src/context/SanityProvider.tsx +10 -8
  26. package/src/hooks/_synchronous-groq-js.mjs +4 -0
  27. package/src/hooks/auth/useAuthState.tsx +0 -2
  28. package/src/hooks/auth/useCurrentUser.tsx +26 -20
  29. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
  30. package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
  31. package/src/hooks/client/useClient.ts +8 -30
  32. package/src/hooks/comlink/useFrameConnection.test.tsx +55 -10
  33. package/src/hooks/comlink/useFrameConnection.ts +39 -43
  34. package/src/hooks/comlink/useManageFavorite.test.ts +106 -0
  35. package/src/hooks/comlink/useManageFavorite.ts +101 -0
  36. package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +77 -0
  37. package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +79 -0
  38. package/src/hooks/comlink/useWindowConnection.test.ts +53 -12
  39. package/src/hooks/comlink/useWindowConnection.ts +69 -29
  40. package/src/hooks/context/useSanityInstance.test.tsx +1 -1
  41. package/src/hooks/context/useSanityInstance.ts +21 -5
  42. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +97 -0
  43. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +274 -0
  44. package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +91 -0
  45. package/src/hooks/datasets/useDatasets.ts +37 -0
  46. package/src/hooks/document/useApplyActions.test.ts +5 -4
  47. package/src/hooks/document/useApplyActions.ts +55 -5
  48. package/src/hooks/document/useDocument.test.ts +2 -2
  49. package/src/hooks/document/useDocument.ts +90 -21
  50. package/src/hooks/document/useDocumentEvent.test.ts +13 -3
  51. package/src/hooks/document/useDocumentEvent.ts +36 -4
  52. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -1
  53. package/src/hooks/document/useDocumentSyncStatus.ts +26 -2
  54. package/src/hooks/document/useEditDocument.test.ts +55 -10
  55. package/src/hooks/document/useEditDocument.ts +159 -31
  56. package/src/hooks/document/usePermissions.ts +82 -0
  57. package/src/hooks/helpers/createCallbackHook.tsx +3 -2
  58. package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
  59. package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
  60. package/src/hooks/infiniteList/useInfiniteList.test.tsx +152 -0
  61. package/src/hooks/infiniteList/useInfiniteList.ts +174 -0
  62. package/src/hooks/paginatedList/usePaginatedList.test.tsx +259 -0
  63. package/src/hooks/paginatedList/usePaginatedList.ts +290 -0
  64. package/src/hooks/preview/usePreview.test.tsx +6 -6
  65. package/src/hooks/preview/usePreview.tsx +12 -9
  66. package/src/hooks/projection/useProjection.test.tsx +218 -0
  67. package/src/hooks/projection/useProjection.ts +147 -0
  68. package/src/hooks/projects/useProject.ts +45 -0
  69. package/src/hooks/projects/useProjects.ts +41 -0
  70. package/src/hooks/query/useQuery.test.tsx +188 -0
  71. package/src/hooks/query/useQuery.ts +103 -0
  72. package/src/hooks/users/useUsers.test.ts +163 -0
  73. package/src/hooks/users/useUsers.ts +107 -0
  74. package/src/utils/getEnv.ts +21 -0
  75. package/src/version.ts +8 -0
  76. package/dist/_chunks-es/context.js +0 -8
  77. package/dist/_chunks-es/context.js.map +0 -1
  78. package/dist/_chunks-es/useLogOut.js +0 -45
  79. package/dist/_chunks-es/useLogOut.js.map +0 -1
  80. package/dist/components.d.ts +0 -111
  81. package/dist/components.js +0 -153
  82. package/dist/components.js.map +0 -1
  83. package/dist/context.d.ts +0 -45
  84. package/dist/context.js +0 -5
  85. package/dist/context.js.map +0 -1
  86. package/dist/hooks.d.ts +0 -3532
  87. package/dist/hooks.js +0 -218
  88. package/dist/hooks.js.map +0 -1
  89. package/src/_exports/components.ts +0 -2
  90. package/src/_exports/context.ts +0 -2
  91. package/src/_exports/hooks.ts +0 -32
  92. package/src/hooks/client/useClient.test.tsx +0 -130
  93. package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
  94. package/src/hooks/documentCollection/useDocuments.ts +0 -135
@@ -45,12 +45,12 @@ const mockDocument: DocumentHandle = {
45
45
 
46
46
  function TestComponent({document}: {document: DocumentHandle}) {
47
47
  const ref = useRef(null)
48
- const {results, isPending} = usePreview({document, ref})
48
+ const {data, isPending} = usePreview({document, ref})
49
49
 
50
50
  return (
51
51
  <div ref={ref}>
52
- <h1>{results?.title}</h1>
53
- <p>{results?.subtitle}</p>
52
+ <h1>{data?.title}</h1>
53
+ <p>{data?.subtitle}</p>
54
54
  {isPending && <div>Pending...</div>}
55
55
  </div>
56
56
  )
@@ -75,7 +75,7 @@ describe('usePreview', () => {
75
75
  test('it only subscribes when element is visible', async () => {
76
76
  // Setup initial state
77
77
  getCurrent.mockReturnValue({
78
- results: {title: 'Initial Title', subtitle: 'Initial Subtitle'},
78
+ data: {title: 'Initial Title', subtitle: 'Initial Subtitle'},
79
79
  isPending: false,
80
80
  })
81
81
  const eventsUnsubscribe = vi.fn()
@@ -139,7 +139,7 @@ describe('usePreview', () => {
139
139
  intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
140
140
  await resolvePromise
141
141
  getCurrent.mockReturnValue({
142
- results: {title: 'Resolved Title', subtitle: 'Resolved Subtitle'},
142
+ data: {title: 'Resolved Title', subtitle: 'Resolved Subtitle'},
143
143
  isPending: false,
144
144
  })
145
145
  subscriber?.()
@@ -156,7 +156,7 @@ describe('usePreview', () => {
156
156
  delete window.IntersectionObserver
157
157
 
158
158
  getCurrent.mockReturnValue({
159
- results: {title: 'Fallback Title', subtitle: 'Fallback Subtitle'},
159
+ data: {title: 'Fallback Title', subtitle: 'Fallback Subtitle'},
160
160
  isPending: false,
161
161
  })
162
162
  subscribe.mockImplementation(() => vi.fn())
@@ -5,7 +5,8 @@ import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxj
5
5
  import {useSanityInstance} from '../context/useSanityInstance'
6
6
 
7
7
  /**
8
- * @alpha
8
+ * @beta
9
+ * @category Types
9
10
  */
10
11
  export interface UsePreviewOptions {
11
12
  document: DocumentHandle
@@ -13,23 +14,25 @@ export interface UsePreviewOptions {
13
14
  }
14
15
 
15
16
  /**
16
- * @alpha
17
+ * @beta
18
+ * @category Types
17
19
  */
18
20
  export interface UsePreviewResults {
19
21
  /** The results of resolving the document’s preview values */
20
- results: PreviewValue
22
+ data: PreviewValue
21
23
  /** True when preview values are being refreshed */
22
24
  isPending: boolean
23
25
  }
24
26
 
25
27
  /**
26
- * @alpha
28
+ * @beta
27
29
  *
28
- * The `usePreview` hook takes a document (via a `DocumentHandle`) and returns its resolved preview values,
30
+ * Returns the preview values of a document (specified via a `DocumentHandle`),
29
31
  * including the document’s `title`, `subtitle`, `media`, and `status`. These values are live and will update in realtime.
30
32
  * To reduce unnecessary network requests for resolving the preview values, an optional `ref` can be passed to the hook so that preview
31
33
  * resolution will only occur if the `ref` is intersecting the current viewport.
32
34
  *
35
+ * @category Documents
33
36
  * @param options - The document handle for the document you want to resolve preview values for, and an optional ref
34
37
  * @returns The preview values for the given document and a boolean to indicate whether the resolution is pending
35
38
  *
@@ -37,7 +40,7 @@ export interface UsePreviewResults {
37
40
  * ```
38
41
  * // PreviewComponent.jsx
39
42
  * export default function PreviewComponent({ document }) {
40
- * const { results: { title, subtitle, media }, isPending } = usePreview({ document })
43
+ * const { data: { title, subtitle, media }, isPending } = usePreview({ document })
41
44
  * return (
42
45
  * <article style={{ opacity: isPending ? 0.5 : 1}}>
43
46
  * {media?.type === 'image-asset' ? <img src={media.url} alt='' /> : ''}
@@ -48,12 +51,12 @@ export interface UsePreviewResults {
48
51
  * }
49
52
  *
50
53
  * // DocumentList.jsx
51
- * const { results, isPending } = useDocuments({ filter: '_type == "movie"' })
54
+ * const { data, isPending } = useDocuments({ filter: '_type == "movie"' })
52
55
  * return (
53
56
  * <div>
54
57
  * <h1>Movies</h1>
55
58
  * <ul>
56
- * {isPending ? 'Loading…' : results.map(movie => (
59
+ * {isPending ? 'Loading…' : data.map(movie => (
57
60
  * <li key={movie._id}>
58
61
  * <Suspense fallback='Loading…'>
59
62
  * <PreviewComponent document={movie} />
@@ -112,7 +115,7 @@ export function usePreview({document: {_id, _type}, ref}: UsePreviewOptions): Us
112
115
  // Create getSnapshot function to return current state
113
116
  const getSnapshot = useCallback(() => {
114
117
  const currentState = stateSource.getCurrent()
115
- if (currentState.results === null) throw resolvePreview(instance, {document: {_id, _type}})
118
+ if (currentState.data === null) throw resolvePreview(instance, {document: {_id, _type}})
116
119
  return currentState as UsePreviewResults
117
120
  }, [_id, _type, instance, stateSource])
118
121
 
@@ -0,0 +1,218 @@
1
+ import {
2
+ type DocumentHandle,
3
+ getProjectionState,
4
+ resolveProjection,
5
+ type ValidProjection,
6
+ } from '@sanity/sdk'
7
+ import {act, render, screen} from '@testing-library/react'
8
+ import {Suspense, useRef} from 'react'
9
+ import {type Mock} from 'vitest'
10
+
11
+ import {useProjection} from './useProjection'
12
+
13
+ // Mock IntersectionObserver
14
+ const mockIntersectionObserver = vi.fn()
15
+ let intersectionObserverCallback: (entries: IntersectionObserverEntry[]) => void
16
+
17
+ beforeAll(() => {
18
+ vi.stubGlobal(
19
+ 'IntersectionObserver',
20
+ class {
21
+ constructor(callback: (entries: IntersectionObserverEntry[]) => void) {
22
+ intersectionObserverCallback = callback
23
+ mockIntersectionObserver(callback)
24
+ }
25
+ observe = vi.fn()
26
+ disconnect = vi.fn()
27
+ },
28
+ )
29
+ })
30
+
31
+ // Mock the projection store
32
+ vi.mock('@sanity/sdk', () => {
33
+ const getCurrent = vi.fn()
34
+ const subscribe = vi.fn()
35
+
36
+ return {
37
+ resolveProjection: vi.fn(),
38
+ getProjectionState: vi.fn().mockReturnValue({getCurrent, subscribe}),
39
+ }
40
+ })
41
+
42
+ vi.mock('../context/useSanityInstance', () => ({
43
+ useSanityInstance: () => ({}),
44
+ }))
45
+
46
+ const mockDocument: DocumentHandle = {
47
+ _id: 'doc1',
48
+ _type: 'exampleType',
49
+ }
50
+
51
+ interface ProjectionResult {
52
+ title: string
53
+ description: string
54
+ }
55
+
56
+ function TestComponent({
57
+ document,
58
+ projection,
59
+ }: {
60
+ document: DocumentHandle
61
+ projection: ValidProjection
62
+ }) {
63
+ const ref = useRef(null)
64
+ const {data, isPending} = useProjection<ProjectionResult>({document, projection, ref})
65
+
66
+ return (
67
+ <div ref={ref}>
68
+ <h1>{data.title}</h1>
69
+ <p>{data.description}</p>
70
+ {isPending && <div>Pending...</div>}
71
+ </div>
72
+ )
73
+ }
74
+
75
+ describe('useProjection', () => {
76
+ let getCurrent: Mock
77
+ let subscribe: Mock
78
+
79
+ beforeEach(() => {
80
+ // @ts-expect-error mock does not need param
81
+ getCurrent = getProjectionState().getCurrent as Mock
82
+ // @ts-expect-error mock does not need param
83
+ subscribe = getProjectionState().subscribe as Mock
84
+
85
+ // Reset all mocks between tests
86
+ getCurrent.mockReset()
87
+ subscribe.mockReset()
88
+ mockIntersectionObserver.mockReset()
89
+ })
90
+
91
+ test('it only subscribes when element is visible', async () => {
92
+ // Setup initial state
93
+ getCurrent.mockReturnValue({
94
+ data: {title: 'Initial Title', description: 'Initial Description'},
95
+ isPending: false,
96
+ })
97
+ const eventsUnsubscribe = vi.fn()
98
+ subscribe.mockImplementation(() => eventsUnsubscribe)
99
+
100
+ render(
101
+ <Suspense fallback={<div>Loading...</div>}>
102
+ <TestComponent document={mockDocument} projection="{name, description}" />
103
+ </Suspense>,
104
+ )
105
+
106
+ // Initially, element is not intersecting
107
+ expect(screen.getByText('Initial Title')).toBeInTheDocument()
108
+ expect(subscribe).not.toHaveBeenCalled()
109
+
110
+ // Simulate element becoming visible
111
+ await act(async () => {
112
+ intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
113
+ })
114
+
115
+ // After element becomes visible, events subscription should be active
116
+ expect(subscribe).toHaveBeenCalled()
117
+ expect(eventsUnsubscribe).not.toHaveBeenCalled()
118
+
119
+ // Simulate element becoming hidden
120
+ await act(async () => {
121
+ intersectionObserverCallback([{isIntersecting: false} as IntersectionObserverEntry])
122
+ })
123
+
124
+ // When hidden, should maintain last known state
125
+ expect(screen.getByText('Initial Title')).toBeInTheDocument()
126
+ expect(eventsUnsubscribe).toHaveBeenCalled()
127
+ })
128
+
129
+ test('it suspends and resolves data when element becomes visible', async () => {
130
+ // Mock the initial state to trigger suspense
131
+ getCurrent.mockReturnValueOnce({
132
+ data: null,
133
+ isPending: true,
134
+ })
135
+
136
+ const resolvedData = {
137
+ data: {title: 'Resolved Title', description: 'Resolved Description'},
138
+ isPending: false,
139
+ }
140
+
141
+ // Mock resolveProjection to return a promise that resolves immediately
142
+ ;(resolveProjection as Mock).mockReturnValueOnce(Promise.resolve(resolvedData))
143
+
144
+ // After suspense resolves, return the resolved data
145
+ getCurrent.mockReturnValue(resolvedData)
146
+
147
+ // Setup subscription that does nothing (we'll manually trigger updates)
148
+ subscribe.mockReturnValue(() => {})
149
+
150
+ render(
151
+ <Suspense fallback={<div>Loading...</div>}>
152
+ <TestComponent document={mockDocument} projection="{title, description}" />
153
+ </Suspense>,
154
+ )
155
+
156
+ await act(async () => {
157
+ intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
158
+ await Promise.resolve()
159
+ })
160
+
161
+ expect(screen.getByText('Resolved Title')).toBeInTheDocument()
162
+ expect(screen.getByText('Resolved Description')).toBeInTheDocument()
163
+ })
164
+
165
+ test('it handles environments without IntersectionObserver', async () => {
166
+ // Temporarily remove IntersectionObserver
167
+ const originalIntersectionObserver = window.IntersectionObserver
168
+ // @ts-expect-error - Intentionally removing IntersectionObserver
169
+ delete window.IntersectionObserver
170
+
171
+ getCurrent.mockReturnValue({
172
+ data: {title: 'Fallback Title', description: 'Fallback Description'},
173
+ isPending: false,
174
+ })
175
+ subscribe.mockImplementation(() => vi.fn())
176
+
177
+ render(
178
+ <Suspense fallback={<div>Loading...</div>}>
179
+ <TestComponent document={mockDocument} projection="{title, description}" />
180
+ </Suspense>,
181
+ )
182
+
183
+ expect(screen.getByText('Fallback Title')).toBeInTheDocument()
184
+
185
+ // Restore IntersectionObserver
186
+ window.IntersectionObserver = originalIntersectionObserver
187
+ })
188
+
189
+ test('it updates when projection changes', async () => {
190
+ getCurrent.mockReturnValue({
191
+ data: {title: 'Initial Title'},
192
+ isPending: false,
193
+ })
194
+ const eventsUnsubscribe = vi.fn()
195
+ subscribe.mockImplementation(() => eventsUnsubscribe)
196
+
197
+ const {rerender} = render(
198
+ <Suspense fallback={<div>Loading...</div>}>
199
+ <TestComponent document={mockDocument} projection="{title}" />
200
+ </Suspense>,
201
+ )
202
+
203
+ // Change projection
204
+ getCurrent.mockReturnValue({
205
+ data: {title: 'Updated Title', description: 'Added Description'},
206
+ isPending: false,
207
+ })
208
+
209
+ rerender(
210
+ <Suspense fallback={<div>Loading...</div>}>
211
+ <TestComponent document={mockDocument} projection="{title, description}" />
212
+ </Suspense>,
213
+ )
214
+
215
+ expect(screen.getByText('Updated Title')).toBeInTheDocument()
216
+ expect(screen.getByText('Added Description')).toBeInTheDocument()
217
+ })
218
+ })
@@ -0,0 +1,147 @@
1
+ import {
2
+ type DocumentHandle,
3
+ getProjectionState,
4
+ resolveProjection,
5
+ type ValidProjection,
6
+ } from '@sanity/sdk'
7
+ import {useCallback, useMemo, useSyncExternalStore} from 'react'
8
+ import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
9
+
10
+ import {useSanityInstance} from '../context/useSanityInstance'
11
+
12
+ /**
13
+ * @public
14
+ * @category Types
15
+ */
16
+ export interface UseProjectionOptions {
17
+ document: DocumentHandle
18
+ projection: ValidProjection
19
+ ref?: React.RefObject<unknown>
20
+ }
21
+
22
+ /**
23
+ * @public
24
+ * @category Types
25
+ */
26
+ export interface UseProjectionResults<TResult extends object> {
27
+ data: TResult
28
+ isPending: boolean
29
+ }
30
+
31
+ /**
32
+ * @beta
33
+ *
34
+ * Returns the projection values of a document (specified via a `DocumentHandle`),
35
+ * based on the provided projection string. These values are live and will update in realtime.
36
+ * To reduce unnecessary network requests for resolving the projection values, an optional `ref` can be passed to the hook so that projection
37
+ * resolution will only occur if the `ref` is intersecting the current viewport.
38
+ *
39
+ * @category Documents
40
+ * @param options - The document handle for the document you want to project values from, the projection string, and an optional ref
41
+ * @returns The projection values for the given document and a boolean to indicate whether the resolution is pending
42
+ *
43
+ * @example Using a projection to render a preview of document
44
+ * ```
45
+ * // ProjectionComponent.jsx
46
+ * export default function ProjectionComponent({ document }) {
47
+ * const ref = useRef(null)
48
+ * const { results: { title, coverImage, authors }, isPending } = useProjection({
49
+ * document,
50
+ * ref,
51
+ * projection: `{
52
+ * title,
53
+ * 'coverImage': cover.asset->url,
54
+ * 'authors': array::join(authors[]->{'name': firstName + ' ' + lastName + ' '}.name, ', ')
55
+ * }`,
56
+ * })
57
+ *
58
+ * return (
59
+ * <article ref={ref} style={{ opacity: isPending ? 0.5 : 1}}>
60
+ * <h2>{title}</h2>
61
+ * <img src={coverImage} alt={title} />
62
+ * <p>{authors}</p>
63
+ * </article>
64
+ * )
65
+ * }
66
+ * ```
67
+ *
68
+ * @example Combining with useInfiniteList to render a collection with specific fields
69
+ * ```
70
+ * // DocumentList.jsx
71
+ * const { data } = useInfiniteList({ filter: '_type == "article"' })
72
+ * return (
73
+ * <div>
74
+ * <h1>Books</h1>
75
+ * <ul>
76
+ * {data.map(book => (
77
+ * <li key={book._id}>
78
+ * <Suspense fallback='Loading…'>
79
+ * <ProjectionComponent
80
+ * document={book}
81
+ * />
82
+ * </Suspense>
83
+ * </li>
84
+ * ))}
85
+ * </ul>
86
+ * </div>
87
+ * )
88
+ * ```
89
+ */
90
+ export function useProjection<TResult extends object>({
91
+ document: {_id, _type},
92
+ projection,
93
+ ref,
94
+ }: UseProjectionOptions): UseProjectionResults<TResult> {
95
+ const instance = useSanityInstance()
96
+
97
+ const stateSource = useMemo(
98
+ () => getProjectionState<TResult>(instance, {document: {_id, _type}, projection}),
99
+ [instance, _id, _type, projection],
100
+ )
101
+
102
+ // Create subscribe function for useSyncExternalStore
103
+ const subscribe = useCallback(
104
+ (onStoreChanged: () => void) => {
105
+ const subscription = new Observable<boolean>((observer) => {
106
+ // for environments that don't have an intersection observer
107
+ if (typeof IntersectionObserver === 'undefined' || typeof HTMLElement === 'undefined') {
108
+ return
109
+ }
110
+
111
+ const intersectionObserver = new IntersectionObserver(
112
+ ([entry]) => observer.next(entry.isIntersecting),
113
+ {rootMargin: '0px', threshold: 0},
114
+ )
115
+ if (ref?.current && ref.current instanceof HTMLElement) {
116
+ intersectionObserver.observe(ref.current)
117
+ }
118
+ return () => intersectionObserver.disconnect()
119
+ })
120
+ .pipe(
121
+ startWith(false),
122
+ distinctUntilChanged(),
123
+ switchMap((isVisible) =>
124
+ isVisible
125
+ ? new Observable<void>((obs) => {
126
+ return stateSource.subscribe(() => obs.next())
127
+ })
128
+ : EMPTY,
129
+ ),
130
+ )
131
+ .subscribe({next: onStoreChanged})
132
+
133
+ return () => subscription.unsubscribe()
134
+ },
135
+ [stateSource, ref],
136
+ )
137
+
138
+ // Create getSnapshot function to return current state
139
+ const getSnapshot = useCallback(() => {
140
+ const currentState = stateSource.getCurrent()
141
+ if (currentState.data === null)
142
+ throw resolveProjection(instance, {document: {_id, _type}, projection})
143
+ return currentState as UseProjectionResults<TResult>
144
+ }, [_id, _type, projection, instance, stateSource])
145
+
146
+ return useSyncExternalStore(subscribe, getSnapshot)
147
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ getProjectState,
3
+ resolveProject,
4
+ type SanityInstance,
5
+ type SanityProject,
6
+ type StateSource,
7
+ } from '@sanity/sdk'
8
+
9
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
10
+
11
+ type UseProject = {
12
+ /**
13
+ *
14
+ * Returns metadata for a given project
15
+ *
16
+ * @category Projects
17
+ * @param projectId - The ID of the project to retrieve metadata for
18
+ * @returns The metadata for the project
19
+ * @example
20
+ * ```tsx
21
+ * function ProjectMetadata({ projectId }: { projectId: string }) {
22
+ * const project = useProject(projectId)
23
+ *
24
+ * return (
25
+ * <figure style={{ backgroundColor: project.metadata.color || 'lavender'}}>
26
+ * <h1>{project.displayName}</h1>
27
+ * </figure>
28
+ * )
29
+ * }
30
+ * ```
31
+ */
32
+ (projectId: string): SanityProject
33
+ }
34
+
35
+ /** @public */
36
+ export const useProject: UseProject = createStateSourceHook({
37
+ // remove `undefined` since we're suspending when that is the case
38
+ getState: getProjectState as (
39
+ instance: SanityInstance,
40
+ projectId: string,
41
+ ) => StateSource<SanityProject>,
42
+ shouldSuspend: (instance, projectId) =>
43
+ getProjectState(instance, projectId).getCurrent() === undefined,
44
+ suspender: resolveProject,
45
+ })
@@ -0,0 +1,41 @@
1
+ import {type SanityProject} from '@sanity/client'
2
+ import {getProjectsState, resolveProjects, type SanityInstance, type StateSource} from '@sanity/sdk'
3
+
4
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
+
6
+ /**
7
+ * @public
8
+ * @category Types
9
+ */
10
+ export type ProjectWithoutMembers = Omit<SanityProject, 'members'>
11
+
12
+ type UseProjects = {
13
+ /**
14
+ *
15
+ * Returns metadata for each project in your organization.
16
+ *
17
+ * @category Projects
18
+ * @returns An array of metadata (minus the projects’ members) for each project in your organization
19
+ * @example
20
+ * ```tsx
21
+ * const projects = useProjects()
22
+ *
23
+ * return (
24
+ * <select>
25
+ * {projects.map((project) => (
26
+ * <option key={project.id}>{project.displayName}</option>
27
+ * ))}
28
+ * </select>
29
+ * )
30
+ * ```
31
+ */
32
+ (): ProjectWithoutMembers[]
33
+ }
34
+
35
+ /** @public */
36
+ export const useProjects: UseProjects = createStateSourceHook({
37
+ // remove `undefined` since we're suspending when that is the case
38
+ getState: getProjectsState as (instance: SanityInstance) => StateSource<ProjectWithoutMembers[]>,
39
+ shouldSuspend: (instance) => getProjectsState(instance).getCurrent() === undefined,
40
+ suspender: resolveProjects,
41
+ })