@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2

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 (168) hide show
  1. package/README.md +103 -155
  2. package/dist/index.d.ts +2488 -2019
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +4435 -1529
  5. package/dist/index.js.map +1 -1
  6. package/package.json +55 -59
  7. package/src/_exports/index.ts +2 -4
  8. package/src/_exports/sdk-react.ts +30 -18
  9. package/src/components/SDKProvider.test.tsx +173 -32
  10. package/src/components/SDKProvider.tsx +87 -28
  11. package/src/components/SanityApp.test.tsx +68 -12
  12. package/src/components/SanityApp.tsx +93 -87
  13. package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
  14. package/src/components/auth/AuthBoundary.test.tsx +7 -7
  15. package/src/components/auth/AuthBoundary.tsx +29 -6
  16. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  17. package/src/components/auth/LoginError.test.tsx +191 -5
  18. package/src/components/auth/LoginError.tsx +104 -61
  19. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  20. package/src/components/errors/ChunkLoadError.tsx +56 -0
  21. package/src/components/errors/CorsErrorComponent.tsx +2 -2
  22. package/src/components/errors/chunkReloadStorage.ts +57 -0
  23. package/src/config/handles.ts +29 -27
  24. package/src/constants.ts +5 -0
  25. package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
  26. package/src/context/ComlinkTokenRefresh.tsx +3 -5
  27. package/src/context/DefaultResourceContext.ts +3 -3
  28. package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
  29. package/src/context/OrganizationResourcesProvider.tsx +111 -0
  30. package/src/context/PerspectiveContext.ts +3 -3
  31. package/src/context/ProjectContext.ts +15 -0
  32. package/src/context/ResourceProvider.test.tsx +93 -90
  33. package/src/context/ResourceProvider.tsx +62 -93
  34. package/src/context/SDKStudioContext.test.tsx +28 -33
  35. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  36. package/src/context/SanityInstanceProvider.tsx +71 -0
  37. package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
  38. package/src/context/WorkbenchTokenRefresh.tsx +61 -0
  39. package/src/context/renderSanityApp.test.tsx +151 -49
  40. package/src/context/renderSanityApp.tsx +12 -8
  41. package/src/context/workbenchToken.ts +63 -0
  42. package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
  43. package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
  44. package/src/hooks/access/useCheckPermissions.ts +24 -0
  45. package/src/hooks/agent/agentActions.test.tsx +2 -2
  46. package/src/hooks/agent/agentActions.ts +52 -72
  47. package/src/hooks/agent/useAgentResourceContext.ts +1 -1
  48. package/src/hooks/applications/useApplication.test-d.ts +30 -0
  49. package/src/hooks/applications/useApplication.ts +22 -0
  50. package/src/hooks/applications/useApplications.test-d.ts +31 -0
  51. package/src/hooks/applications/useApplications.ts +25 -0
  52. package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
  53. package/src/hooks/applications/useDeleteApplication.ts +11 -0
  54. package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
  55. package/src/hooks/applications/useUpdateApplication.ts +11 -0
  56. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
  57. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
  58. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  59. package/src/hooks/client/useClient.test.tsx +1 -4
  60. package/src/hooks/client/useClient.ts +1 -0
  61. package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
  62. package/src/hooks/comlink/useFrameConnection.ts +1 -1
  63. package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
  64. package/src/hooks/comlink/useWindowConnection.ts +2 -3
  65. package/src/hooks/comments/useCommentActions.test.tsx +150 -0
  66. package/src/hooks/comments/useCommentActions.ts +109 -0
  67. package/src/hooks/comments/useCommentList.ts +79 -0
  68. package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
  69. package/src/hooks/comments/useCommentThreads.ts +73 -0
  70. package/src/hooks/comments/useComments.test.tsx +242 -0
  71. package/src/hooks/comments/useComments.ts +59 -0
  72. package/src/hooks/context/useResource.test.tsx +32 -0
  73. package/src/hooks/context/useResource.ts +24 -0
  74. package/src/hooks/context/useSanityInstance.test.tsx +18 -1
  75. package/src/hooks/context/useSanityInstance.ts +6 -6
  76. package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
  77. package/src/hooks/dashboard/useManageFavorite.ts +11 -17
  78. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
  79. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
  80. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
  81. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -2
  82. package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
  83. package/src/hooks/dashboard/useWindowTitle.ts +114 -0
  84. package/src/hooks/datasets/useDatasets.test.tsx +116 -0
  85. package/src/hooks/datasets/useDatasets.ts +33 -13
  86. package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
  87. package/src/hooks/document/useApplyDocumentActions.ts +66 -48
  88. package/src/hooks/document/useCreateDocument.test.tsx +83 -0
  89. package/src/hooks/document/useCreateDocument.ts +117 -0
  90. package/src/hooks/document/useDocument.test.tsx +37 -8
  91. package/src/hooks/document/useDocument.ts +131 -66
  92. package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
  93. package/src/hooks/document/useDocumentEvent.ts +16 -10
  94. package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
  95. package/src/hooks/document/useDocumentPermissions.ts +71 -39
  96. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
  97. package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
  98. package/src/hooks/document/useEditDocument.test.tsx +21 -14
  99. package/src/hooks/document/useEditDocument.ts +163 -17
  100. package/src/hooks/documents/useDocuments.test.tsx +64 -44
  101. package/src/hooks/documents/useDocuments.ts +31 -20
  102. package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
  103. package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
  104. package/src/hooks/helpers/createFetcherHook.ts +69 -0
  105. package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
  106. package/src/hooks/helpers/createMutationHook.tsx +93 -0
  107. package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
  108. package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
  109. package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
  110. package/src/hooks/helpers/useApplyActions.ts +68 -0
  111. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +226 -0
  112. package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
  113. package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
  114. package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
  115. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  116. package/src/hooks/installations/useInstallation.test-d.ts +19 -0
  117. package/src/hooks/installations/useInstallation.ts +22 -0
  118. package/src/hooks/installations/useInstallations.test-d.ts +26 -0
  119. package/src/hooks/installations/useInstallations.ts +25 -0
  120. package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
  121. package/src/hooks/organizations/useOrganization.test.ts +63 -0
  122. package/src/hooks/organizations/useOrganization.ts +34 -0
  123. package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
  124. package/src/hooks/organizations/useOrganizations.test.ts +63 -0
  125. package/src/hooks/organizations/useOrganizations.ts +39 -0
  126. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
  127. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +35 -22
  128. package/src/hooks/presence/usePresence.test.tsx +0 -15
  129. package/src/hooks/presence/usePresence.ts +22 -20
  130. package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
  131. package/src/hooks/presence/usePresenceForDocument.ts +104 -0
  132. package/src/hooks/presence/useReportPresence.test.tsx +202 -0
  133. package/src/hooks/presence/useReportPresence.ts +170 -0
  134. package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
  135. package/src/hooks/preview/useDocumentPreview.tsx +12 -14
  136. package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
  137. package/src/hooks/projection/useDocumentProjection.ts +81 -2
  138. package/src/hooks/projects/useProject.test-d.ts +56 -0
  139. package/src/hooks/projects/useProject.test.tsx +120 -0
  140. package/src/hooks/projects/useProject.ts +42 -43
  141. package/src/hooks/projects/useProjects.test-d.ts +60 -0
  142. package/src/hooks/projects/useProjects.test.ts +45 -98
  143. package/src/hooks/projects/useProjects.ts +17 -32
  144. package/src/hooks/query/useQuery.test.tsx +6 -5
  145. package/src/hooks/query/useQuery.ts +94 -17
  146. package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
  147. package/src/hooks/releases/useActiveReleases.ts +28 -24
  148. package/src/hooks/releases/useAllReleases.test.tsx +93 -0
  149. package/src/hooks/releases/useAllReleases.ts +62 -0
  150. package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
  151. package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
  152. package/src/hooks/releases/usePerspective.test.tsx +18 -10
  153. package/src/hooks/releases/usePerspective.ts +13 -17
  154. package/src/hooks/users/useUser.test.tsx +3 -9
  155. package/src/hooks/users/useUser.ts +4 -8
  156. package/src/hooks/users/useUsers.test.tsx +103 -7
  157. package/src/hooks/users/useUsers.ts +38 -5
  158. package/src/utils/resolveOrgResources.test.ts +111 -0
  159. package/src/utils/resolveOrgResources.ts +69 -0
  160. package/src/hooks/context/useDefaultResource.test.tsx +0 -25
  161. package/src/hooks/context/useDefaultResource.ts +0 -30
  162. package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
  163. package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
  164. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
  165. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
  166. package/src/hooks/datasets/useDatasets.test.ts +0 -66
  167. package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
  168. package/src/hooks/projects/useProject.test.ts +0 -79
@@ -1,112 +1,59 @@
1
- import {getProjectsState, type SanityInstance} from '@sanity/sdk'
1
+ import {type Project, projects, type StateSource} from '@sanity/sdk'
2
+ import {type FetcherSnapshot} from '@sanity/sdk/_internal'
3
+ import {type Observable} from 'rxjs'
2
4
  import {beforeEach, describe, expect, it, vi} from 'vitest'
3
5
 
4
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
6
+ import {renderHook} from '../../../test/test-utils'
7
+ import {useProjects} from './useProjects'
5
8
 
6
- // Mock dependencies
7
- vi.mock('@sanity/sdk', () => ({
8
- getProjectsState: vi.fn(() => ({
9
- getCurrent: vi.fn(() => undefined), // Mocking getCurrent to satisfy the call within shouldSuspend
10
- })),
11
- resolveProjects: vi.fn(),
12
- }))
13
- vi.mock('../helpers/createStateSourceHook', () => ({
14
- createStateSourceHook: vi.fn(),
15
- }))
9
+ vi.mock('@sanity/sdk', async (importOriginal) => {
10
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
11
+ return {...original, projects: {getState: vi.fn(), resolveState: vi.fn(), refetch: vi.fn()}}
12
+ })
13
+
14
+ const stateSource = (current: Project[] | undefined): StateSource<FetcherSnapshot<Project[]>> => {
15
+ const snapshot = current
16
+ ? {status: 'success', data: current, error: undefined, isFetching: false, dataUpdatedAt: 1}
17
+ : {
18
+ status: 'pending',
19
+ data: undefined,
20
+ error: undefined,
21
+ isFetching: true,
22
+ dataUpdatedAt: undefined,
23
+ }
24
+ return {
25
+ getCurrent: vi.fn(() => snapshot),
26
+ subscribe: vi.fn(() => () => {}),
27
+ get observable(): Observable<unknown> {
28
+ throw new Error('Not implemented')
29
+ },
30
+ } as unknown as StateSource<FetcherSnapshot<Project[]>>
31
+ }
32
+
33
+ const sanityInstance = expect.objectContaining({config: expect.any(Object)})
16
34
 
17
35
  describe('useProjects', () => {
18
- // Use beforeEach to reset modules and ensure mocks are fresh for each test
19
36
  beforeEach(() => {
20
- vi.resetModules()
21
- // Re-mock dependencies for each test after resetModules
22
- vi.mock('@sanity/sdk', () => ({
23
- getProjectsState: vi.fn(() => ({
24
- getCurrent: vi.fn(() => undefined),
25
- })),
26
- resolveProjects: vi.fn(),
27
- }))
28
- vi.mock('../helpers/createStateSourceHook', () => ({
29
- createStateSourceHook: vi.fn(),
30
- }))
31
- })
32
-
33
- it('should call createStateSourceHook with correct arguments on import', async () => {
34
- // Dynamically import the hook *after* mocks are set up and modules reset
35
- await import('./useProjects')
36
-
37
- // Check if createStateSourceHook was called during the module evaluation (import)
38
- expect(createStateSourceHook).toHaveBeenCalled()
39
- expect(createStateSourceHook).toHaveBeenCalledWith(
40
- expect.objectContaining({
41
- getState: expect.any(Function),
42
- shouldSuspend: expect.any(Function),
43
- suspender: expect.any(Function), // Actual function reference doesn't matter here as it's mocked
44
- // Note: getConfig is not used in useProjects
45
- }),
46
- )
37
+ vi.clearAllMocks()
38
+ vi.mocked(projects.getState).mockReturnValue(stateSource([{id: 'a'}] as unknown as Project[]))
47
39
  })
48
40
 
49
- it('shouldSuspend should call getProjectsState and getCurrent', async () => {
50
- // Dynamically import the hook *after* mocks are set up and modules reset
51
- await import('./useProjects')
52
-
53
- // Get the arguments passed to createStateSourceHook
54
- const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
55
- expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
56
- const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
57
- const shouldSuspend = createStateSourceHookArgs.shouldSuspend
58
-
59
- // Mock instance for the test call
60
- const mockInstance = {} as SanityInstance // Use specific type
61
-
62
- // Call the shouldSuspend function with both required parameters
63
- const result = shouldSuspend(mockInstance, undefined) // Pass undefined for options
64
-
65
- // Assert that getProjectsState was called with the correct arguments
66
- const mockGetProjectsState = getProjectsState as ReturnType<typeof vi.fn>
67
- expect(mockGetProjectsState).toHaveBeenCalledWith(mockInstance, undefined)
68
-
69
- // Assert that getCurrent was called on the result of getProjectsState
70
- expect(mockGetProjectsState.mock.results.length).toBeGreaterThan(0)
71
- const getProjectsStateMockResult = mockGetProjectsState.mock.results[0].value
72
- expect(getProjectsStateMockResult.getCurrent).toHaveBeenCalled()
73
-
74
- // Assert the result of shouldSuspend based on the mocked getCurrent value
75
- expect(result).toBe(true) // Since getCurrent is mocked to return undefined
41
+ it('reads the projects fetcher with the passed options', () => {
42
+ renderHook(() => useProjects({organizationId: 'org123'}))
43
+ expect(projects.getState).toHaveBeenCalledWith(sanityInstance, {organizationId: 'org123'})
76
44
  })
77
45
 
78
- it('should call createStateSourceHook with correct getState function signature', async () => {
79
- await import('./useProjects')
80
-
81
- const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
82
- expect(mockCreateStateSourceHook).toHaveBeenCalled()
83
-
84
- const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
85
- const getState = createStateSourceHookArgs.getState
86
-
87
- // Test that getState can handle the new options parameter
88
- const mockInstance = {} as SanityInstance
89
- const mockOptions = {organizationId: 'org123', includeMembers: false}
90
-
91
- // This should not throw
92
- expect(() => getState(mockInstance, mockOptions)).not.toThrow()
46
+ it('returns the fetcher data in the result envelope', () => {
47
+ const {result} = renderHook(() => useProjects())
48
+ expect(result.current.data).toEqual([{id: 'a'}])
49
+ expect(result.current.isFetching).toBe(false)
50
+ expect(typeof result.current.refetch).toBe('function')
93
51
  })
94
52
 
95
- it('should handle different parameter combinations in shouldSuspend', async () => {
96
- await import('./useProjects')
97
-
98
- const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
99
- const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
100
- const shouldSuspend = createStateSourceHookArgs.shouldSuspend
101
-
102
- const mockInstance = {} as SanityInstance
103
-
104
- // Test with different options
105
- expect(() => shouldSuspend(mockInstance, undefined)).not.toThrow()
106
- expect(() => shouldSuspend(mockInstance, {organizationId: 'org123'})).not.toThrow()
107
- expect(() => shouldSuspend(mockInstance, {includeMembers: false})).not.toThrow()
108
- expect(() =>
109
- shouldSuspend(mockInstance, {organizationId: 'org123', includeMembers: false}),
110
- ).not.toThrow()
53
+ it('suspends via the projects fetcher until data is available', () => {
54
+ vi.mocked(projects.getState).mockReturnValue(stateSource(undefined))
55
+ vi.mocked(projects.resolveState).mockReturnValue(new Promise(() => {}))
56
+ renderHook(() => useProjects())
57
+ expect(projects.resolveState).toHaveBeenCalled()
111
58
  })
112
59
  })
@@ -1,33 +1,18 @@
1
- import {type SanityProject} from '@sanity/client'
2
- import {getProjectsState, resolveProjects, type SanityInstance, type StateSource} from '@sanity/sdk'
1
+ import {type Project, projects, type ProjectsOptions} from '@sanity/sdk'
3
2
 
4
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
-
6
- /**
7
- * @public
8
- * @category Types
9
- * @interface
10
- */
11
- export type ProjectWithoutMembers = Omit<SanityProject, 'members'>
12
-
13
- /**
14
- * @public
15
- * @category Types
16
- */
17
- type UseProjects = <TIncludeMembers extends boolean = false>(options?: {
18
- organizationId?: string
19
- includeMembers?: TIncludeMembers
20
- }) => TIncludeMembers extends true ? SanityProject[] : ProjectWithoutMembers[]
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
21
4
 
22
5
  /**
23
6
  * Returns metadata for each project you have access to.
24
7
  *
25
8
  * @category Projects
26
9
  * @param options - Configuration options
27
- * @returns An array of project metadata. If includeMembers is true, returns full SanityProject objects. Otherwise, returns ProjectWithoutMembers objects.
10
+ * @returns A {@link FetcherHookResult} whose `data` is an array of project
11
+ * metadata. `members` is included only when `includeMembers: true`; `features`
12
+ * is included unless `includeFeatures: false`.
28
13
  * @example
29
14
  * ```tsx
30
- * const projects = useProjects()
15
+ * const {data: projects} = useProjects()
31
16
  *
32
17
  * return (
33
18
  * <select>
@@ -39,18 +24,18 @@ type UseProjects = <TIncludeMembers extends boolean = false>(options?: {
39
24
  * ```
40
25
  * @example
41
26
  * ```tsx
42
- * const projectsWithMembers = useProjects({ includeMembers: true })
43
- * const projectsWithoutMembers = useProjects({ includeMembers: false })
27
+ * const {data: projects} = useProjects()
28
+ * const {data: projectsWithFeatures} = useProjects()
29
+ * const {data: projectsWithMembers} = useProjects({includeMembers: true})
30
+ * const {data: projectsWithoutMembers} = useProjects({includeMembers: false})
31
+ * const {data: projectsWithoutFeatures} = useProjects({includeFeatures: false})
44
32
  * ```
45
33
  * @public
46
34
  * @function
47
35
  */
48
- export const useProjects: UseProjects = createStateSourceHook({
49
- getState: getProjectsState as (
50
- instance: SanityInstance,
51
- options?: {organizationId?: string; includeMembers?: boolean},
52
- ) => StateSource<SanityProject[] | ProjectWithoutMembers[]>,
53
- shouldSuspend: (instance, options) =>
54
- getProjectsState(instance, options).getCurrent() === undefined,
55
- suspender: resolveProjects,
56
- }) as UseProjects
36
+ export const useProjects = createFetcherHook(projects) as <
37
+ IncludeMembers extends boolean = false,
38
+ IncludeFeatures extends boolean = true,
39
+ >(
40
+ options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
41
+ ) => FetcherHookResult<Project<IncludeMembers, IncludeFeatures>[]>
@@ -33,7 +33,7 @@ describe('useQuery', () => {
33
33
  } as StateSource<unknown>)
34
34
 
35
35
  function TestComponent() {
36
- const {data, isPending} = useQuery<string>({query: 'test query'})
36
+ const {data, isPending} = useQuery({query: 'test query'})
37
37
  return (
38
38
  <div data-testid="output">
39
39
  {data} - {isPending ? 'pending' : 'not pending'}
@@ -42,7 +42,7 @@ describe('useQuery', () => {
42
42
  }
43
43
 
44
44
  render(
45
- <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
45
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
46
46
  <TestComponent />
47
47
  </ResourceProvider>,
48
48
  )
@@ -82,13 +82,14 @@ describe('useQuery', () => {
82
82
  )
83
83
 
84
84
  function TestComponent() {
85
- const {data} = useQuery<string>({query: 'test query'})
85
+ const {data} = useQuery({query: 'test query'})
86
86
  return <div data-testid="output">{data}</div>
87
87
  }
88
88
 
89
89
  render(
90
90
  <ResourceProvider
91
- resource={{projectId: 'p', dataset: 'd'}}
91
+ projectId="p"
92
+ dataset="d"
92
93
  fallback={<div data-testid="fallback">Loading...</div>}
93
94
  >
94
95
  <TestComponent />
@@ -163,7 +164,7 @@ describe('useQuery', () => {
163
164
  }
164
165
 
165
166
  render(
166
- <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
167
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
167
168
  <WrapperComponent />
168
169
  </ResourceProvider>,
169
170
  )
@@ -1,25 +1,99 @@
1
- import {
2
- getQueryKey,
3
- getQueryState,
4
- parseQueryKey,
5
- type QueryOptions,
6
- resolveQuery,
7
- } from '@sanity/sdk'
1
+ import {getQueryState, type QueryOptions, resolveQuery} from '@sanity/sdk'
2
+ import {getQueryKey, parseQueryKey} from '@sanity/sdk/_internal'
3
+ import {type SanityQueryResult} from 'groq'
8
4
  import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
9
5
 
10
- import {type ResourceHandle} from '../../config/handles'
11
6
  import {useSanityInstance} from '../context/useSanityInstance'
12
- import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
7
+ import {
8
+ useNormalizedResourceOptions,
9
+ type WithResourceNameSupport,
10
+ } from '../helpers/useNormalizedResourceOptions'
11
+ import {trackHookUsage} from '../helpers/useTrackHookUsage'
12
+ /**
13
+ * Hook options for useQuery, supporting both direct resource and resourceName.
14
+ * @beta
15
+ */
16
+ type UseQueryOptions<
17
+ TQuery extends string = string,
18
+ TDataset extends string = string,
19
+ TProjectId extends string = string,
20
+ > = WithResourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
13
21
 
14
- /** Options for useQuery: QueryOptions with resource made optional (resolved from context) */
15
- type ReactQueryOptions = Omit<QueryOptions, 'resource' | 'resourceName'> & ResourceHandle
22
+ // Overload 1: Inferred Type (using Typegen)
23
+ /**
24
+ * @public
25
+ * Executes a GROQ query, inferring the result type from the query string and options.
26
+ * Leverages Sanity Typegen if configured for enhanced type safety.
27
+ *
28
+ * @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
29
+ * @returns An object containing `data` (typed based on the query) and `isPending` (for transitions).
30
+ *
31
+ * @example Basic usage (Inferred Type)
32
+ * ```tsx
33
+ * import {useQuery} from '@sanity/sdk-react'
34
+ * import {defineQuery} from 'groq'
35
+ *
36
+ * const myQuery = defineQuery(`*[_type == "movie"]{_id, title}`)
37
+ *
38
+ * function MovieList() {
39
+ * // Typegen infers the return type for data
40
+ * const {data} = useQuery({ query: myQuery })
41
+ *
42
+ * return (
43
+ * <div>
44
+ * <h2>Movies</h2>
45
+ * <ul>
46
+ * {data.map(movie => <li key={movie._id}>{movie.title}</li>)}
47
+ * </ul>
48
+ * </div>
49
+ * )
50
+ * }
51
+ * // Suspense boundary should wrap <MovieList /> for initial load
52
+ * ```
53
+ *
54
+ * @example Using parameters (Inferred Type)
55
+ * ```tsx
56
+ * import {useQuery} from '@sanity/sdk-react'
57
+ * import {defineQuery} from 'groq'
58
+ *
59
+ * const myQuery = defineQuery(`*[_type == "movie" && _id == $id][0]`)
60
+ *
61
+ * function MovieDetails({movieId}: {movieId: string}) {
62
+ * // Typegen infers the return type based on query and params
63
+ * const {data, isPending} = useQuery({
64
+ * query: myQuery,
65
+ * params: { id: movieId }
66
+ * })
67
+ *
68
+ * return (
69
+ * // utilize `isPending` to signal to users that new data is coming in
70
+ * // (e.g. the `movieId` changed and we're loading in the new one)
71
+ * <div style={{ opacity: isPending ? 0.5 : 1 }}>
72
+ * {data ? <h1>{data.title}</h1> : <p>Movie not found</p>}
73
+ * </div>
74
+ * )
75
+ * }
76
+ * ```
77
+ */
78
+ export function useQuery<
79
+ TQuery extends string = string,
80
+ TDataset extends string = string,
81
+ TProjectId extends string = string,
82
+ >(
83
+ options: UseQueryOptions<TQuery, TDataset, TProjectId>,
84
+ ): {
85
+ /** The query result, typed based on the GROQ query string */
86
+ data: SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>
87
+ /** True if a query transition is in progress */
88
+ isPending: boolean
89
+ }
16
90
 
17
- // Overload 1: Explicit Type Provided
91
+ // Overload 2: Explicit Type Provided
18
92
  /**
19
93
  * @public
20
94
  * Executes a GROQ query with an explicitly provided result type `TData`.
21
95
  *
22
- * @param options - Configuration for the query, including `query`, optional `params`, `resource`, etc.
96
+ * @param options - Configuration for the query, including `query`, optional `params`, `projectId`, `dataset`, etc.
23
97
  * @returns An object containing `data` (cast to `TData`) and `isPending` (indicates whether a query resolution is pending; note that Suspense handles initial loading states). *
24
98
  * @example Manually typed query result
25
99
  * ```tsx
@@ -43,7 +117,7 @@ type ReactQueryOptions = Omit<QueryOptions, 'resource' | 'resourceName'> & Resou
43
117
  * }
44
118
  * ```
45
119
  */
46
- export function useQuery<TData>(options: ReactQueryOptions): {
120
+ export function useQuery<TData>(options: WithResourceNameSupport<QueryOptions>): {
47
121
  /** The query result, cast to the provided type TData */
48
122
  data: TData
49
123
  /** True if another query is resolving in the background (suspense handles the initial loading state) */
@@ -63,16 +137,18 @@ export function useQuery<TData>(options: ReactQueryOptions): {
63
137
  * - Subscribes to changes, providing real-time updates.
64
138
  * - Integrates with React Suspense for handling initial loading states.
65
139
  * - Uses React Transitions for managing loading states during query/parameter changes (indicated by `isPending`).
140
+ * - Supports type inference based on the GROQ query when using Sanity Typegen.
66
141
  * - Allows specifying an explicit return type `TData` for the query result.
67
142
  *
68
143
  * @category GROQ
69
144
  */
70
- export function useQuery(options: ReactQueryOptions): {
145
+ export function useQuery(options: WithResourceNameSupport<QueryOptions>): {
71
146
  data: unknown
72
147
  isPending: boolean
73
148
  } {
74
149
  // Implementation returns unknown, overloads define specifics
75
150
  const instance = useSanityInstance()
151
+ trackHookUsage(instance, 'useQuery')
76
152
 
77
153
  // Normalize options: resolve resourceName to resource and strip resourceName
78
154
  const normalized = useNormalizedResourceOptions(options)
@@ -81,7 +157,7 @@ export function useQuery(options: ReactQueryOptions): {
81
157
  const [isPending, startTransition] = useTransition()
82
158
 
83
159
  // Get the unique key for this query and its options (using normalized options)
84
- const queryKey = getQueryKey(normalized)
160
+ const queryKey = getQueryKey(instance, normalized)
85
161
  // Use a deferred state to avoid immediate re-renders when the query changes
86
162
  const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
87
163
 
@@ -123,11 +199,12 @@ export function useQuery(options: ReactQueryOptions): {
123
199
  const currentSignal = ref.current.signal
124
200
  const deferred = parseQueryKey(deferredQueryKey)
125
201
 
202
+ // eslint-disable-next-line react-hooks/refs -- intentional during suspended render; see comment above.
126
203
  throw resolveQuery(instance, {...deferred, signal: currentSignal})
127
204
  }
128
205
 
129
206
  // Subscribe to updates and get the current data
130
207
  // useSyncExternalStore ensures the component re-renders when the data changes
131
- const data = useSyncExternalStore(subscribe, getCurrent) as unknown
208
+ const data = useSyncExternalStore(subscribe, getCurrent) as SanityQueryResult
132
209
  return useMemo(() => ({data, isPending}), [data, isPending])
133
210
  }
@@ -20,14 +20,14 @@ describe('useActiveReleases', () => {
20
20
  vi.clearAllMocks()
21
21
  })
22
22
 
23
- it('should suspend when initial state is undefined', () => {
23
+ it('suspends until the releases state source emits, then resolves with the data', async () => {
24
24
  const mockSubject = new BehaviorSubject<ReleaseDocument[] | undefined>(undefined)
25
25
  const mockStateSource = {
26
26
  subscribe: vi.fn((callback) => {
27
27
  const subscription = mockSubject.subscribe(callback)
28
28
  return () => subscription.unsubscribe()
29
29
  }),
30
- getCurrent: vi.fn(() => undefined),
30
+ getCurrent: vi.fn(() => mockSubject.getValue()),
31
31
  observable: mockSubject,
32
32
  }
33
33
 
@@ -43,22 +43,28 @@ describe('useActiveReleases', () => {
43
43
  },
44
44
  {
45
45
  wrapper: ({children}) => (
46
- <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
46
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
47
47
  {children}
48
48
  </ResourceProvider>
49
49
  ),
50
50
  },
51
51
  )
52
52
 
53
- // Verify that the hook threw a promise (suspended)
54
53
  expect(result.current).toBeInstanceOf(Promise)
55
54
  expect(mockStateSource.getCurrent).toHaveBeenCalled()
55
+
56
+ const resolved: ReleaseDocument[] = [
57
+ {_id: 'release1', _type: 'release'} as unknown as ReleaseDocument,
58
+ ]
59
+ mockSubject.next(resolved)
60
+
61
+ await expect(result.current).resolves.toEqual(resolved)
56
62
  })
57
63
 
58
64
  it('should resolve with releases when data is available', () => {
59
65
  const mockReleases: ReleaseDocument[] = [
60
- {_id: 'release1', _type: 'release'} as ReleaseDocument,
61
- {_id: 'release2', _type: 'release'} as ReleaseDocument,
66
+ {_id: 'release1', _type: 'release'} as unknown as ReleaseDocument,
67
+ {_id: 'release2', _type: 'release'} as unknown as ReleaseDocument,
62
68
  ]
63
69
 
64
70
  const mockSubject = new BehaviorSubject<ReleaseDocument[]>(mockReleases)
@@ -75,7 +81,7 @@ describe('useActiveReleases', () => {
75
81
 
76
82
  const {result} = renderHook(() => useActiveReleases(), {
77
83
  wrapper: ({children}) => (
78
- <ResourceProvider resource={{projectId: 'p', dataset: 'd'}} fallback={<p>Loading...</p>}>
84
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
79
85
  {children}
80
86
  </ResourceProvider>
81
87
  ),
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type DatasetHandle,
2
3
  type DocumentResource,
3
4
  getActiveReleasesState,
4
5
  type ReleaseDocument,
@@ -7,18 +8,38 @@ import {
7
8
  } from '@sanity/sdk'
8
9
  import {filter, firstValueFrom} from 'rxjs'
9
10
 
10
- import {type ResourceHandle} from '../../config/handles'
11
11
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
- import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
12
+ import {
13
+ useNormalizedResourceOptions,
14
+ type WithResourceNameSupport,
15
+ } from '../helpers/useNormalizedResourceOptions'
16
+
17
+ type UseActiveReleasesValue = {
18
+ (options?: {resource?: DocumentResource}): ReleaseDocument[]
19
+ }
20
+
21
+ const useActiveReleasesValue: UseActiveReleasesValue = createStateSourceHook({
22
+ getState: getActiveReleasesState as (
23
+ instance: SanityInstance,
24
+ options?: {resource?: DocumentResource},
25
+ ) => StateSource<ReleaseDocument[]>,
26
+ shouldSuspend: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
27
+ getActiveReleasesState(instance, options ?? {}).getCurrent() === undefined,
28
+ suspender: (instance: SanityInstance, options?: {resource?: DocumentResource}) =>
29
+ firstValueFrom(
30
+ getActiveReleasesState(instance, options ?? {}).observable.pipe(filter(Boolean)),
31
+ ),
32
+ })
13
33
 
14
34
  /**
15
35
  * @public
16
-
36
+ * @function
37
+ *
17
38
  * Returns the active releases for the current project,
18
39
  * represented as a list of release documents.
19
40
  *
20
41
  * @returns The active releases for the current project.
21
- * @category Projects
42
+ * @category Releases
22
43
  * @example
23
44
  * ```tsx
24
45
  * import {useActiveReleases} from '@sanity/sdk-react'
@@ -26,26 +47,9 @@ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOpti
26
47
  * const activeReleases = useActiveReleases()
27
48
  * ```
28
49
  */
29
- type UseActiveReleases = {
30
- (options?: ResourceHandle | undefined): ReleaseDocument[]
31
- }
32
-
33
- const useActiveReleasesValue = createStateSourceHook({
34
- getState: getActiveReleasesState as (
35
- instance: SanityInstance,
36
- options: {resource: DocumentResource},
37
- ) => StateSource<ReleaseDocument[]>,
38
- shouldSuspend: (instance: SanityInstance, options: {resource: DocumentResource}) =>
39
- getActiveReleasesState(instance, options).getCurrent() === undefined,
40
- suspender: (instance: SanityInstance, options: {resource: DocumentResource}) =>
41
- firstValueFrom(getActiveReleasesState(instance, options).observable.pipe(filter(Boolean))),
42
- })
43
-
44
- /**
45
- * @public
46
- * @function
47
- */
48
- export const useActiveReleases: UseActiveReleases = (options: ResourceHandle | undefined) => {
50
+ export function useActiveReleases(
51
+ options?: WithResourceNameSupport<DatasetHandle> | undefined,
52
+ ): ReleaseDocument[] {
49
53
  const normalizedOptions = useNormalizedResourceOptions(options ?? {})
50
54
  return useActiveReleasesValue(normalizedOptions)
51
55
  }
@@ -0,0 +1,93 @@
1
+ import {getAllReleasesState, type ReleaseDocument} 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 {ResourceProvider} from '../../context/ResourceProvider'
7
+ import {useAllReleases} from './useAllReleases'
8
+
9
+ vi.mock('@sanity/sdk', async () => {
10
+ const actual = await vi.importActual('@sanity/sdk')
11
+ return {
12
+ ...actual,
13
+ getAllReleasesState: vi.fn(),
14
+ }
15
+ })
16
+
17
+ describe('useAllReleases', () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks()
20
+ })
21
+
22
+ it('suspends until the releases state source emits, then resolves with the data', async () => {
23
+ const mockSubject = new BehaviorSubject<ReleaseDocument[] | undefined>(undefined)
24
+ const mockStateSource = {
25
+ subscribe: vi.fn((callback) => {
26
+ const subscription = mockSubject.subscribe(callback)
27
+ return () => subscription.unsubscribe()
28
+ }),
29
+ getCurrent: vi.fn(() => mockSubject.getValue()),
30
+ observable: mockSubject,
31
+ }
32
+
33
+ vi.mocked(getAllReleasesState).mockReturnValue(mockStateSource)
34
+
35
+ const {result} = renderHook(
36
+ () => {
37
+ try {
38
+ return useAllReleases()
39
+ } catch (e) {
40
+ return e
41
+ }
42
+ },
43
+ {
44
+ wrapper: ({children}) => (
45
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
46
+ {children}
47
+ </ResourceProvider>
48
+ ),
49
+ },
50
+ )
51
+
52
+ expect(result.current).toBeInstanceOf(Promise)
53
+ expect(mockStateSource.getCurrent).toHaveBeenCalled()
54
+
55
+ const resolved: ReleaseDocument[] = [
56
+ {_id: 'r-active', _type: 'system.release', state: 'active'} as ReleaseDocument,
57
+ ]
58
+ mockSubject.next(resolved)
59
+
60
+ await expect(result.current).resolves.toEqual(resolved)
61
+ })
62
+
63
+ it('returns every release including archived and published once loaded', () => {
64
+ const mockReleases: ReleaseDocument[] = [
65
+ {_id: 'r-active', _type: 'system.release', state: 'active'} as ReleaseDocument,
66
+ {_id: 'r-archived', _type: 'system.release', state: 'archived'} as ReleaseDocument,
67
+ {_id: 'r-published', _type: 'system.release', state: 'published'} as ReleaseDocument,
68
+ ]
69
+
70
+ const mockSubject = new BehaviorSubject<ReleaseDocument[]>(mockReleases)
71
+ const mockStateSource = {
72
+ subscribe: vi.fn((callback) => {
73
+ const subscription = mockSubject.subscribe(callback)
74
+ return () => subscription.unsubscribe()
75
+ }),
76
+ getCurrent: vi.fn(() => mockReleases),
77
+ observable: mockSubject,
78
+ }
79
+
80
+ vi.mocked(getAllReleasesState).mockReturnValue(mockStateSource)
81
+
82
+ const {result} = renderHook(() => useAllReleases(), {
83
+ wrapper: ({children}) => (
84
+ <ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
85
+ {children}
86
+ </ResourceProvider>
87
+ ),
88
+ })
89
+
90
+ expect(result.current).toEqual(mockReleases)
91
+ expect(mockStateSource.getCurrent).toHaveBeenCalled()
92
+ })
93
+ })