@sanity/sdk-react 3.0.0-rc.0 → 3.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 (160) hide show
  1. package/README.md +103 -155
  2. package/dist/index.d.ts +1564 -682
  3. package/dist/index.js +1118 -612
  4. package/dist/index.js.map +1 -1
  5. package/package.json +51 -54
  6. package/src/_exports/index.ts +2 -4
  7. package/src/_exports/sdk-react.ts +26 -17
  8. package/src/components/SDKProvider.test.tsx +173 -32
  9. package/src/components/SDKProvider.tsx +87 -28
  10. package/src/components/SanityApp.test.tsx +68 -12
  11. package/src/components/SanityApp.tsx +94 -87
  12. package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
  13. package/src/components/auth/AuthBoundary.test.tsx +26 -8
  14. package/src/components/auth/AuthBoundary.tsx +28 -6
  15. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  16. package/src/components/auth/LoginError.test.tsx +191 -5
  17. package/src/components/auth/LoginError.tsx +103 -59
  18. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  19. package/src/components/errors/ChunkLoadError.tsx +56 -0
  20. package/src/components/errors/CorsErrorComponent.tsx +2 -2
  21. package/src/components/errors/chunkReloadStorage.ts +57 -0
  22. package/src/config/handles.ts +29 -27
  23. package/src/constants.ts +5 -0
  24. package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
  25. package/src/context/DefaultResourceContext.ts +3 -3
  26. package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
  27. package/src/context/OrganizationResourcesProvider.tsx +111 -0
  28. package/src/context/PerspectiveContext.ts +3 -3
  29. package/src/context/ProjectContext.ts +15 -0
  30. package/src/context/ResourceProvider.test.tsx +72 -90
  31. package/src/context/ResourceProvider.tsx +59 -93
  32. package/src/context/SDKStudioContext.test.tsx +28 -33
  33. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  34. package/src/context/SanityInstanceProvider.tsx +71 -0
  35. package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
  36. package/src/context/WorkbenchTokenRefresh.tsx +61 -0
  37. package/src/context/renderSanityApp.test.tsx +151 -49
  38. package/src/context/renderSanityApp.tsx +12 -8
  39. package/src/context/workbenchToken.ts +63 -0
  40. package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
  41. package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
  42. package/src/hooks/access/useCheckPermissions.ts +24 -0
  43. package/src/hooks/agent/agentActions.test.tsx +1 -1
  44. package/src/hooks/agent/agentActions.ts +51 -71
  45. package/src/hooks/applications/useApplication.test-d.ts +30 -0
  46. package/src/hooks/applications/useApplication.ts +22 -0
  47. package/src/hooks/applications/useApplications.test-d.ts +31 -0
  48. package/src/hooks/applications/useApplications.ts +25 -0
  49. package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
  50. package/src/hooks/applications/useCreateUserApplication.ts +11 -0
  51. package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
  52. package/src/hooks/applications/useDeleteApplication.ts +11 -0
  53. package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
  54. package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
  55. package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
  56. package/src/hooks/applications/useUpdateApplication.ts +11 -0
  57. package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
  58. package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
  59. package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
  60. package/src/hooks/applications/useUserApplication.ts +14 -0
  61. package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
  62. package/src/hooks/applications/useUserApplications.ts +14 -0
  63. package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
  64. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
  65. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  66. package/src/hooks/client/useClient.test.tsx +1 -4
  67. package/src/hooks/client/useClient.ts +1 -0
  68. package/src/hooks/context/useResource.test.tsx +32 -0
  69. package/src/hooks/context/useResource.ts +24 -0
  70. package/src/hooks/context/useSanityInstance.test.tsx +72 -3
  71. package/src/hooks/context/useSanityInstance.ts +30 -8
  72. package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
  73. package/src/hooks/dashboard/useManageFavorite.ts +10 -16
  74. package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
  75. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
  76. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
  77. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +1 -2
  78. package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
  79. package/src/hooks/dashboard/useWindowTitle.ts +114 -0
  80. package/src/hooks/datasets/useDatasets.test.tsx +116 -0
  81. package/src/hooks/datasets/useDatasets.ts +33 -13
  82. package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
  83. package/src/hooks/document/useApplyDocumentActions.ts +66 -48
  84. package/src/hooks/document/useCreateDocument.test.tsx +83 -0
  85. package/src/hooks/document/useCreateDocument.ts +117 -0
  86. package/src/hooks/document/useDocument.test.tsx +37 -8
  87. package/src/hooks/document/useDocument.ts +131 -66
  88. package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
  89. package/src/hooks/document/useDocumentEvent.ts +16 -10
  90. package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
  91. package/src/hooks/document/useDocumentPermissions.ts +71 -39
  92. package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
  93. package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
  94. package/src/hooks/document/useEditDocument.test.tsx +21 -14
  95. package/src/hooks/document/useEditDocument.ts +163 -17
  96. package/src/hooks/documents/useDocuments.test.tsx +64 -44
  97. package/src/hooks/documents/useDocuments.ts +36 -20
  98. package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
  99. package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
  100. package/src/hooks/helpers/createFetcherHook.ts +69 -0
  101. package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
  102. package/src/hooks/helpers/createMutationHook.tsx +93 -0
  103. package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
  104. package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
  105. package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
  106. package/src/hooks/helpers/useApplyActions.ts +68 -0
  107. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
  108. package/src/hooks/helpers/useNormalizedResourceOptions.ts +92 -50
  109. package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
  110. package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
  111. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  112. package/src/hooks/installations/useInstallation.test-d.ts +19 -0
  113. package/src/hooks/installations/useInstallation.ts +22 -0
  114. package/src/hooks/installations/useInstallations.test-d.ts +26 -0
  115. package/src/hooks/installations/useInstallations.ts +25 -0
  116. package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
  117. package/src/hooks/organizations/useOrganization.test.ts +63 -0
  118. package/src/hooks/organizations/useOrganization.ts +34 -0
  119. package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
  120. package/src/hooks/organizations/useOrganizations.test.ts +63 -0
  121. package/src/hooks/organizations/useOrganizations.ts +39 -0
  122. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
  123. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +40 -22
  124. package/src/hooks/presence/usePresence.test.tsx +0 -15
  125. package/src/hooks/presence/usePresence.ts +9 -20
  126. package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
  127. package/src/hooks/preview/useDocumentPreview.tsx +10 -8
  128. package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
  129. package/src/hooks/projection/useDocumentProjection.ts +81 -2
  130. package/src/hooks/projects/useProject.test-d.ts +56 -0
  131. package/src/hooks/projects/useProject.test.tsx +120 -0
  132. package/src/hooks/projects/useProject.ts +42 -43
  133. package/src/hooks/projects/useProjects.test-d.ts +60 -0
  134. package/src/hooks/projects/useProjects.test.ts +45 -98
  135. package/src/hooks/projects/useProjects.ts +19 -26
  136. package/src/hooks/query/useQuery.test.tsx +6 -5
  137. package/src/hooks/query/useQuery.ts +91 -9
  138. package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
  139. package/src/hooks/releases/useActiveReleases.ts +28 -24
  140. package/src/hooks/releases/useAllReleases.test.tsx +93 -0
  141. package/src/hooks/releases/useAllReleases.ts +62 -0
  142. package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
  143. package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
  144. package/src/hooks/releases/usePerspective.test.tsx +18 -10
  145. package/src/hooks/releases/usePerspective.ts +13 -17
  146. package/src/hooks/users/useUser.test.tsx +3 -9
  147. package/src/hooks/users/useUser.ts +2 -0
  148. package/src/hooks/users/useUsers.test.tsx +103 -7
  149. package/src/hooks/users/useUsers.ts +37 -3
  150. package/src/utils/resolveOrgResources.test.ts +111 -0
  151. package/src/utils/resolveOrgResources.ts +69 -0
  152. package/src/hooks/context/useDefaultResource.test.tsx +0 -25
  153. package/src/hooks/context/useDefaultResource.ts +0 -30
  154. package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
  155. package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
  156. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
  157. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
  158. package/src/hooks/datasets/useDatasets.test.ts +0 -66
  159. package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
  160. package/src/hooks/projects/useProject.test.ts +0 -79
@@ -0,0 +1,120 @@
1
+ import {createSanityInstance, type Project, project, type StateSource} from '@sanity/sdk'
2
+ import {type FetcherSnapshot} from '@sanity/sdk/_internal'
3
+ import {type ReactNode} from 'react'
4
+ import {type Observable} from 'rxjs'
5
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
6
+
7
+ import {renderHook} from '../../../test/test-utils'
8
+ import {ResourceProvider} from '../../context/ResourceProvider'
9
+ import {SanityInstanceContext} from '../../context/SanityInstanceContext'
10
+ import {useProject} from './useProject'
11
+
12
+ vi.mock('@sanity/sdk', async (importOriginal) => {
13
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
14
+ return {...original, project: {getState: vi.fn(), resolveState: vi.fn()}}
15
+ })
16
+
17
+ const stateSource = (current: Project | undefined): StateSource<FetcherSnapshot<Project>> => {
18
+ // Cache the snapshot: useSyncExternalStore requires a referentially stable current value.
19
+ const snapshot = current
20
+ ? {status: 'success', data: current, error: undefined, isFetching: false, dataUpdatedAt: 1}
21
+ : {
22
+ status: 'pending',
23
+ data: undefined,
24
+ error: undefined,
25
+ isFetching: true,
26
+ dataUpdatedAt: undefined,
27
+ }
28
+ return {
29
+ getCurrent: vi.fn(() => snapshot),
30
+ subscribe: vi.fn(() => () => {}),
31
+ get observable(): Observable<unknown> {
32
+ throw new Error('Not implemented')
33
+ },
34
+ } as unknown as StateSource<FetcherSnapshot<Project>>
35
+ }
36
+
37
+ const sanityInstance = expect.objectContaining({config: expect.any(Object)})
38
+
39
+ describe('useProject', () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks()
42
+ vi.mocked(project.getState).mockReturnValue(stateSource({id: 'p'} as unknown as Project))
43
+ })
44
+
45
+ it('resolves the projectId from the instance config resource', () => {
46
+ // test-utils wraps with ResourceProvider projectId="test" dataset="test".
47
+ renderHook(() => useProject())
48
+ expect(project.getState).toHaveBeenCalledWith(
49
+ sanityInstance,
50
+ expect.objectContaining({projectId: 'test'}),
51
+ )
52
+ })
53
+
54
+ it('lets an explicit projectId override the ambient resource', () => {
55
+ renderHook(() => useProject({projectId: 'explicit-project'}))
56
+ expect(project.getState).toHaveBeenCalledWith(
57
+ sanityInstance,
58
+ expect.objectContaining({projectId: 'explicit-project'}),
59
+ )
60
+ })
61
+
62
+ it('resolves the projectId from an explicit resource when the config has none', () => {
63
+ renderHook(() => useProject(), {
64
+ wrapper: ({children}: {children: ReactNode}) => (
65
+ <ResourceProvider
66
+ resource={{projectId: 'resource-project', dataset: 'production'}}
67
+ fallback={null}
68
+ >
69
+ {children}
70
+ </ResourceProvider>
71
+ ),
72
+ })
73
+ expect(project.getState).toHaveBeenCalledWith(
74
+ sanityInstance,
75
+ expect.objectContaining({projectId: 'resource-project'}),
76
+ )
77
+ })
78
+
79
+ it('resolves a dataset-less projectId config for project-scoped use', () => {
80
+ renderHook(() => useProject(), {
81
+ wrapper: ({children}: {children: ReactNode}) => (
82
+ <ResourceProvider projectId="config-project" fallback={null}>
83
+ {children}
84
+ </ResourceProvider>
85
+ ),
86
+ })
87
+ // A dataset-less config can't form a DatasetResource; the projectId is carried
88
+ // via ProjectContext and injected so project-scoped reads still resolve it.
89
+ expect(project.getState).toHaveBeenCalledWith(
90
+ sanityInstance,
91
+ expect.objectContaining({projectId: 'config-project'}),
92
+ )
93
+ })
94
+
95
+ it('resolves a bare projectId from a ResourceProvider when the parent instance has no config', () => {
96
+ // An instance with no project/dataset config and no ambient
97
+ // resource, then a projectId-only ResourceProvider.
98
+ const emptyInstance = createSanityInstance({})
99
+ renderHook(() => useProject(), {
100
+ wrapper: ({children}: {children: ReactNode}) => (
101
+ <SanityInstanceContext.Provider value={emptyInstance}>
102
+ <ResourceProvider projectId="bare-project" fallback={null}>
103
+ {children}
104
+ </ResourceProvider>
105
+ </SanityInstanceContext.Provider>
106
+ ),
107
+ })
108
+ expect(project.getState).toHaveBeenCalledWith(
109
+ emptyInstance,
110
+ expect.objectContaining({projectId: 'bare-project'}),
111
+ )
112
+ })
113
+
114
+ it('suspends via the project fetcher until project data is available', () => {
115
+ vi.mocked(project.getState).mockReturnValue(stateSource(undefined))
116
+ vi.mocked(project.resolveState).mockReturnValue(new Promise(() => {}))
117
+ renderHook(() => useProject())
118
+ expect(project.resolveState).toHaveBeenCalled()
119
+ })
120
+ })
@@ -1,50 +1,49 @@
1
- import {
2
- getProjectState,
3
- type ProjectHandle,
4
- resolveProject,
5
- type SanityInstance,
6
- type SanityProject,
7
- type StateSource,
8
- } from '@sanity/sdk'
1
+ import {type Project, project, type ProjectOptions} from '@sanity/sdk'
9
2
 
10
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
4
+ import {useResolvedProjectId} from '../helpers/useResolvedProjectId'
11
5
 
12
- type UseProject = {
13
- /**
14
- *
15
- * Returns metadata for a given project
16
- *
17
- * @category Projects
18
- * @param projectHandle - An optional project handle identifying which project to retrieve metadata for
19
- * @returns The metadata for the project
20
- * @example
21
- * ```tsx
22
- * function ProjectMetadata({ projectId }: { projectId: string }) {
23
- * const project = useProject({ projectId })
24
- *
25
- * return (
26
- * <figure style={{ backgroundColor: project.metadata.color || 'lavender'}}>
27
- * <h1>{project.displayName}</h1>
28
- * </figure>
29
- * )
30
- * }
31
- * ```
32
- */
33
- (projectHandle?: ProjectHandle): SanityProject
34
- }
6
+ const useProjectBase = createFetcherHook(project)
35
7
 
36
8
  /**
9
+ * Returns metadata for a given project.
10
+ *
11
+ * @category Projects
12
+ * @param options - Configuration options
13
+ * @returns A {@link FetcherHookResult} whose `data` is the metadata for the
14
+ * project. `members` is included only when `includeMembers: true`; `features`
15
+ * is included unless `includeFeatures: false`.
16
+ * @example
17
+ * ```tsx
18
+ * function ProjectMetadata({projectId}: {projectId: string}) {
19
+ * const {data: project} = useProject({projectId})
20
+ *
21
+ * return (
22
+ * <figure style={{backgroundColor: project.metadata.color || 'lavender'}}>
23
+ * <h1>{project.displayName}</h1>
24
+ * </figure>
25
+ * )
26
+ * }
27
+ * ```
28
+ * @example
29
+ * ```tsx
30
+ * const {data: projectWithMembersAndFeatures} = useProject({projectId})
31
+ * const {data: projectWithMembers} = useProject({projectId, includeMembers: true})
32
+ * const {data: projectWithoutMembers} = useProject({projectId, includeMembers: false})
33
+ * const {data: projectWithoutFeatures} = useProject({projectId, includeFeatures: false})
34
+ * ```
35
+ * @remarks
36
+ * The `projectId` is resolved in order from:
37
+ * 1. an explicit `projectId` option
38
+ * 2. A legacy ProjectContext (e.g. a `<ResourceProvider projectId="…">` with no dataset), then
39
+ * 3. The active resource (`ResourceProvider`/`SDKProvider`)
40
+ * 4. `instance.config`.
37
41
  * @public
38
42
  * @function
39
43
  */
40
- export const useProject: UseProject = createStateSourceHook({
41
- // remove `undefined` since we're suspending when that is the case
42
- getState: getProjectState as (
43
- instance: SanityInstance,
44
- projectHandle?: ProjectHandle,
45
- ) => StateSource<SanityProject>,
46
- shouldSuspend: (instance: SanityInstance, projectHandle?: ProjectHandle) =>
47
- getProjectState(instance, projectHandle as ProjectHandle).getCurrent() === undefined,
48
- suspender: (instance: SanityInstance, projectHandle?: ProjectHandle) =>
49
- resolveProject(instance, projectHandle as ProjectHandle),
50
- }) as UseProject
44
+ export const useProject = ((options?: ProjectOptions<boolean, boolean>) => {
45
+ const projectId = useResolvedProjectId(options)
46
+ return useProjectBase(projectId ? {...options, projectId} : options)
47
+ }) as <IncludeMembers extends boolean = true, IncludeFeatures extends boolean = true>(
48
+ options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
49
+ ) => FetcherHookResult<Project<IncludeMembers, IncludeFeatures>>
@@ -0,0 +1,60 @@
1
+ import {type Project, type ProjectMember} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {useProjects} from './useProjects'
5
+
6
+ test('useProjects — no args: features included, members omitted', () => {
7
+ expectTypeOf(useProjects().data).toEqualTypeOf<Project<false, true>[]>()
8
+ })
9
+
10
+ test('useProjects — returns the FetcherHookResult envelope', () => {
11
+ const result = useProjects()
12
+ expectTypeOf(result.isFetching).toEqualTypeOf<boolean>()
13
+ expectTypeOf(result.error).toEqualTypeOf<unknown>()
14
+ expectTypeOf(result.refetch).toEqualTypeOf<() => Promise<Project<false, true>[]>>()
15
+ })
16
+
17
+ test('useProjects — includeMembers: true adds members to the type', () => {
18
+ expectTypeOf(useProjects({includeMembers: true}).data).toEqualTypeOf<Project<true, true>[]>()
19
+ type Result = ReturnType<typeof useProjects<true, true>>
20
+ expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<ProjectMember[]>()
21
+ })
22
+
23
+ test('useProjects — includeFeatures: false drops features from the type', () => {
24
+ expectTypeOf(useProjects({includeFeatures: false}).data).toEqualTypeOf<Project<false, false>[]>()
25
+ })
26
+
27
+ test('useProjects — both flags true → both arrays present', () => {
28
+ expectTypeOf(useProjects({includeMembers: true, includeFeatures: true}).data).toEqualTypeOf<
29
+ Project<true, true>[]
30
+ >()
31
+ })
32
+
33
+ test('useProjects — both flags false → bare base shape', () => {
34
+ expectTypeOf(useProjects({includeMembers: false, includeFeatures: false}).data).toEqualTypeOf<
35
+ Project<false, false>[]
36
+ >()
37
+ type Result = ReturnType<typeof useProjects<false, false>>
38
+ expectTypeOf<Result['data'][number]['id']>().toEqualTypeOf<string>()
39
+ })
40
+
41
+ test('useProjects — rejects non-boolean flag values', () => {
42
+ // @ts-expect-error — includeMembers must be a boolean
43
+ void useProjects({includeMembers: 'yes'})
44
+ })
45
+
46
+ test('useProjects — organizationId alone does not change the data shape', () => {
47
+ expectTypeOf(useProjects({organizationId: 'org_123'}).data).toEqualTypeOf<
48
+ Project<false, true>[]
49
+ >()
50
+ })
51
+
52
+ test('useProjects — non-literal boolean flag makes members optional', () => {
53
+ const includeMembers = false as boolean
54
+ expectTypeOf(useProjects({includeMembers}).data).toEqualTypeOf<Project<boolean, true>[]>()
55
+ type Result = ReturnType<typeof useProjects<boolean, true>>
56
+ expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
57
+ expectTypeOf<Pick<Result['data'][number], 'members'>>().toEqualTypeOf<{
58
+ members?: ProjectMember[]
59
+ }>()
60
+ })
@@ -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,26 @@
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'
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
5
4
 
6
5
  /**
7
6
  * @public
8
7
  * @category Types
9
8
  * @interface
9
+ * @deprecated use the Project type directly.
10
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[]
11
+ export type ProjectWithoutMembers = Project
21
12
 
22
13
  /**
23
14
  * Returns metadata for each project you have access to.
24
15
  *
25
16
  * @category Projects
26
17
  * @param options - Configuration options
27
- * @returns An array of project metadata. If includeMembers is true, returns full SanityProject objects. Otherwise, returns ProjectWithoutMembers objects.
18
+ * @returns A {@link FetcherHookResult} whose `data` is an array of project
19
+ * metadata. `members` is included only when `includeMembers: true`; `features`
20
+ * is included unless `includeFeatures: false`.
28
21
  * @example
29
22
  * ```tsx
30
- * const projects = useProjects()
23
+ * const {data: projects} = useProjects()
31
24
  *
32
25
  * return (
33
26
  * <select>
@@ -39,18 +32,18 @@ type UseProjects = <TIncludeMembers extends boolean = false>(options?: {
39
32
  * ```
40
33
  * @example
41
34
  * ```tsx
42
- * const projectsWithMembers = useProjects({ includeMembers: true })
43
- * const projectsWithoutMembers = useProjects({ includeMembers: false })
35
+ * const {data: projects} = useProjects()
36
+ * const {data: projectsWithFeatures} = useProjects()
37
+ * const {data: projectsWithMembers} = useProjects({includeMembers: true})
38
+ * const {data: projectsWithoutMembers} = useProjects({includeMembers: false})
39
+ * const {data: projectsWithoutFeatures} = useProjects({includeFeatures: false})
44
40
  * ```
45
41
  * @public
46
42
  * @function
47
43
  */
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
44
+ export const useProjects = createFetcherHook(projects) as <
45
+ IncludeMembers extends boolean = false,
46
+ IncludeFeatures extends boolean = true,
47
+ >(
48
+ options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
49
+ ) => 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
  )