@sanity/sdk-react 2.10.0 → 2.11.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 (55) hide show
  1. package/dist/index.d.ts +257 -200
  2. package/dist/index.js +368 -257
  3. package/dist/index.js.map +1 -1
  4. package/package.json +21 -24
  5. package/src/_exports/index.ts +2 -0
  6. package/src/_exports/sdk-react.ts +4 -0
  7. package/src/components/SDKProvider.test.tsx +5 -12
  8. package/src/components/SDKProvider.tsx +26 -24
  9. package/src/components/errors/CorsErrorComponent.tsx +2 -2
  10. package/src/config/handles.ts +55 -0
  11. package/src/constants.ts +5 -0
  12. package/src/context/DefaultResourceContext.ts +10 -0
  13. package/src/context/PerspectiveContext.ts +12 -0
  14. package/src/context/ResourceProvider.test.tsx +2 -2
  15. package/src/context/ResourceProvider.tsx +53 -49
  16. package/src/hooks/agent/agentActions.ts +55 -38
  17. package/src/hooks/context/useResource.test.tsx +32 -0
  18. package/src/hooks/context/useResource.ts +24 -0
  19. package/src/hooks/context/useSanityInstance.test.tsx +42 -111
  20. package/src/hooks/context/useSanityInstance.ts +28 -50
  21. package/src/hooks/dashboard/useDispatchIntent.test.ts +5 -1
  22. package/src/hooks/dashboard/useDispatchIntent.ts +3 -3
  23. package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
  24. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +1 -5
  25. package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +42 -77
  26. package/src/hooks/document/useApplyDocumentActions.ts +29 -63
  27. package/src/hooks/document/useDocument.ts +5 -7
  28. package/src/hooks/document/useDocumentEvent.ts +4 -3
  29. package/src/hooks/document/useDocumentPermissions.test.tsx +58 -150
  30. package/src/hooks/document/useDocumentPermissions.ts +78 -55
  31. package/src/hooks/document/useEditDocument.test.tsx +25 -60
  32. package/src/hooks/document/useEditDocument.ts +1 -1
  33. package/src/hooks/documents/useDocuments.ts +13 -8
  34. package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
  35. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
  36. package/src/hooks/helpers/useNormalizedResourceOptions.ts +85 -47
  37. package/src/hooks/organizations/useOrganization.test-d.ts +53 -0
  38. package/src/hooks/organizations/useOrganization.test.ts +65 -0
  39. package/src/hooks/organizations/useOrganization.ts +40 -0
  40. package/src/hooks/organizations/useOrganizations.test-d.ts +55 -0
  41. package/src/hooks/organizations/useOrganizations.test.ts +85 -0
  42. package/src/hooks/organizations/useOrganizations.ts +45 -0
  43. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +23 -9
  44. package/src/hooks/presence/usePresence.ts +4 -11
  45. package/src/hooks/preview/useDocumentPreview.tsx +4 -7
  46. package/src/hooks/projection/useDocumentProjection.ts +5 -7
  47. package/src/hooks/projects/useProject.test-d.ts +49 -0
  48. package/src/hooks/projects/useProject.ts +33 -41
  49. package/src/hooks/projects/useProjects.test-d.ts +49 -0
  50. package/src/hooks/projects/useProjects.ts +17 -23
  51. package/src/hooks/query/useQuery.ts +1 -1
  52. package/src/hooks/releases/useActiveReleases.ts +6 -6
  53. package/src/hooks/releases/usePerspective.ts +7 -12
  54. package/src/hooks/users/useUser.ts +1 -1
  55. package/src/hooks/users/useUsers.ts +1 -1
@@ -0,0 +1,40 @@
1
+ import {
2
+ getOrganizationState,
3
+ type Organization,
4
+ type OrganizationOptions,
5
+ resolveOrganization,
6
+ } from '@sanity/sdk'
7
+
8
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
9
+
10
+ /**
11
+ * Returns metadata for a given organisation.
12
+ *
13
+ * @category Organizations
14
+ * @param options - Configuration options
15
+ * @returns The metadata for the organisation. `members` is included only when
16
+ * `includeMembers: true`; `features` is included only when `includeFeatures: true`.
17
+ * @example
18
+ * ```tsx
19
+ * function OrganizationName({organizationId}: {organizationId: string}) {
20
+ * const organization = useOrganization({organizationId})
21
+ *
22
+ * return <h1>{organization.name}</h1>
23
+ * }
24
+ * ```
25
+ * @example
26
+ * ```tsx
27
+ * const organizationWithMembers = useOrganization({organizationId, includeMembers: true})
28
+ * const organizationWithFeatures = useOrganization({organizationId, includeFeatures: true})
29
+ * ```
30
+ * @public
31
+ * @function
32
+ */
33
+ export const useOrganization = createStateSourceHook({
34
+ getState: getOrganizationState,
35
+ shouldSuspend: (instance, ...params) =>
36
+ getOrganizationState(instance, ...params).getCurrent() === undefined,
37
+ suspender: resolveOrganization,
38
+ }) as <IncludeMembers extends boolean = false, IncludeFeatures extends boolean = false>(
39
+ options: OrganizationOptions<IncludeMembers, IncludeFeatures>,
40
+ ) => Organization<IncludeMembers, IncludeFeatures>
@@ -0,0 +1,55 @@
1
+ import {type OrganizationMember, type Organizations} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {useOrganizations} from './useOrganizations'
5
+
6
+ test('useOrganizations — no args: members and features both omitted', () => {
7
+ expectTypeOf(useOrganizations()).toEqualTypeOf<Organizations<false, false>>()
8
+ })
9
+
10
+ test('useOrganizations — includeMembers: true adds members to the type', () => {
11
+ expectTypeOf(useOrganizations({includeMembers: true})).toEqualTypeOf<Organizations<true, false>>()
12
+ type Result = ReturnType<typeof useOrganizations<true, false>>
13
+ expectTypeOf<Result[number]['members']>().toEqualTypeOf<OrganizationMember[]>()
14
+ })
15
+
16
+ test('useOrganizations — includeFeatures: true adds features to the type', () => {
17
+ expectTypeOf(useOrganizations({includeFeatures: true})).toEqualTypeOf<
18
+ Organizations<false, true>
19
+ >()
20
+ })
21
+
22
+ test('useOrganizations — both flags true → both arrays present', () => {
23
+ expectTypeOf(useOrganizations({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
24
+ Organizations<true, true>
25
+ >()
26
+ })
27
+
28
+ test('useOrganizations — both flags false → bare base shape', () => {
29
+ expectTypeOf(useOrganizations({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
30
+ Organizations<false, false>
31
+ >()
32
+ type Result = ReturnType<typeof useOrganizations<false, false>>
33
+ expectTypeOf<Result[number]['id']>().toEqualTypeOf<string>()
34
+ })
35
+
36
+ test('useOrganizations — rejects non-boolean flag values', () => {
37
+ // @ts-expect-error — includeMembers must be a boolean
38
+ void useOrganizations({includeMembers: 'yes'})
39
+ })
40
+
41
+ test('useOrganizations — includeImplicitMemberships does not change the data shape', () => {
42
+ expectTypeOf(useOrganizations({includeImplicitMemberships: true})).toEqualTypeOf<
43
+ Organizations<false, false>
44
+ >()
45
+ })
46
+
47
+ test('useOrganizations — non-literal boolean flag makes members optional', () => {
48
+ const includeMembers = false as boolean
49
+ expectTypeOf(useOrganizations({includeMembers})).toEqualTypeOf<Organizations<boolean, false>>()
50
+ type Result = ReturnType<typeof useOrganizations<boolean, false>>
51
+ expectTypeOf<Result[number]['members']>().toEqualTypeOf<OrganizationMember[] | undefined>()
52
+ expectTypeOf<Pick<Result[number], 'members'>>().toEqualTypeOf<{
53
+ members?: OrganizationMember[]
54
+ }>()
55
+ })
@@ -0,0 +1,85 @@
1
+ import {getOrganizationsState, type SanityInstance} from '@sanity/sdk'
2
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
+
6
+ vi.mock('@sanity/sdk', () => ({
7
+ getOrganizationsState: vi.fn(() => ({
8
+ getCurrent: vi.fn(() => undefined),
9
+ })),
10
+ resolveOrganizations: vi.fn(),
11
+ }))
12
+ vi.mock('../helpers/createStateSourceHook', () => ({
13
+ createStateSourceHook: vi.fn(),
14
+ }))
15
+
16
+ describe('useOrganizations', () => {
17
+ beforeEach(() => {
18
+ vi.resetModules()
19
+ vi.mock('@sanity/sdk', () => ({
20
+ getOrganizationsState: vi.fn(() => ({
21
+ getCurrent: vi.fn(() => undefined),
22
+ })),
23
+ resolveOrganizations: vi.fn(),
24
+ }))
25
+ vi.mock('../helpers/createStateSourceHook', () => ({
26
+ createStateSourceHook: vi.fn(),
27
+ }))
28
+ })
29
+
30
+ it('should call createStateSourceHook with correct arguments on import', async () => {
31
+ await import('./useOrganizations')
32
+
33
+ expect(createStateSourceHook).toHaveBeenCalled()
34
+ expect(createStateSourceHook).toHaveBeenCalledWith(
35
+ expect.objectContaining({
36
+ getState: expect.any(Function),
37
+ shouldSuspend: expect.any(Function),
38
+ suspender: expect.any(Function),
39
+ }),
40
+ )
41
+ })
42
+
43
+ it('shouldSuspend should call getOrganizationsState and getCurrent', async () => {
44
+ await import('./useOrganizations')
45
+
46
+ const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
47
+ expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
48
+ const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
49
+ const shouldSuspend = createStateSourceHookArgs.shouldSuspend
50
+
51
+ const mockInstance = {} as SanityInstance
52
+
53
+ const result = shouldSuspend(mockInstance, undefined)
54
+
55
+ const mockGetOrganizationsState = getOrganizationsState as ReturnType<typeof vi.fn>
56
+ expect(mockGetOrganizationsState).toHaveBeenCalledWith(mockInstance, undefined)
57
+
58
+ expect(mockGetOrganizationsState.mock.results.length).toBeGreaterThan(0)
59
+ const getOrganizationsStateMockResult = mockGetOrganizationsState.mock.results[0].value
60
+ expect(getOrganizationsStateMockResult.getCurrent).toHaveBeenCalled()
61
+
62
+ expect(result).toBe(true)
63
+ })
64
+
65
+ it('should handle different parameter combinations in shouldSuspend', async () => {
66
+ await import('./useOrganizations')
67
+
68
+ const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
69
+ const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
70
+ const shouldSuspend = createStateSourceHookArgs.shouldSuspend
71
+
72
+ const mockInstance = {} as SanityInstance
73
+
74
+ expect(() => shouldSuspend(mockInstance, undefined)).not.toThrow()
75
+ expect(() => shouldSuspend(mockInstance, {includeMembers: true})).not.toThrow()
76
+ expect(() => shouldSuspend(mockInstance, {includeFeatures: true})).not.toThrow()
77
+ expect(() =>
78
+ shouldSuspend(mockInstance, {
79
+ includeMembers: true,
80
+ includeFeatures: true,
81
+ includeImplicitMemberships: true,
82
+ }),
83
+ ).not.toThrow()
84
+ })
85
+ })
@@ -0,0 +1,45 @@
1
+ import {
2
+ getOrganizationsState,
3
+ type Organizations,
4
+ type OrganizationsOptions,
5
+ resolveOrganizations,
6
+ } from '@sanity/sdk'
7
+
8
+ import {createStateSourceHook} from '../helpers/createStateSourceHook'
9
+
10
+ /**
11
+ * Returns metadata for each organisation the current user has access to.
12
+ *
13
+ * @category Organizations
14
+ * @param options - Configuration options
15
+ * @returns An array of organisation metadata. `members` is included only when
16
+ * `includeMembers: true`; `features` is included only when `includeFeatures: true`.
17
+ * @example
18
+ * ```tsx
19
+ * const organizations = useOrganizations()
20
+ *
21
+ * return (
22
+ * <select>
23
+ * {organizations.map((organization) => (
24
+ * <option key={organization.id}>{organization.name}</option>
25
+ * ))}
26
+ * </select>
27
+ * )
28
+ * ```
29
+ * @example
30
+ * ```tsx
31
+ * const organizationsWithMembers = useOrganizations({includeMembers: true})
32
+ * const organizationsWithFeatures = useOrganizations({includeFeatures: true})
33
+ * const organizationsIncludingImplicit = useOrganizations({includeImplicitMemberships: true})
34
+ * ```
35
+ * @public
36
+ * @function
37
+ */
38
+ export const useOrganizations = createStateSourceHook({
39
+ getState: getOrganizationsState,
40
+ shouldSuspend: (instance, ...params) =>
41
+ getOrganizationsState(instance, ...params).getCurrent() === undefined,
42
+ suspender: resolveOrganizations,
43
+ }) as <IncludeMembers extends boolean = false, IncludeFeatures extends boolean = false>(
44
+ options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
45
+ ) => Organizations<IncludeMembers, IncludeFeatures>
@@ -1,8 +1,17 @@
1
- import {createGroqSearchFilter, type DocumentHandle, type QueryOptions} from '@sanity/sdk'
1
+ import {
2
+ createGroqSearchFilter,
3
+ type DocumentHandle,
4
+ isDatasetResource,
5
+ type QueryOptions,
6
+ } from '@sanity/sdk'
7
+ import {pickProperties} from '@sanity/sdk/_internal'
2
8
  import {type SortOrderingItem} from '@sanity/types'
3
9
  import {useCallback, useMemo, useState} from 'react'
4
10
 
5
- import {useSanityInstance} from '../context/useSanityInstance'
11
+ import {
12
+ useNormalizedResourceOptions,
13
+ type WithResourceNameSupport,
14
+ } from '../helpers/useNormalizedResourceOptions'
6
15
  import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
7
16
  import {useQuery} from '../query/useQuery'
8
17
 
@@ -16,7 +25,9 @@ export interface PaginatedDocumentsOptions<
16
25
  TDocumentType extends string = string,
17
26
  TDataset extends string = string,
18
27
  TProjectId extends string = string,
19
- > extends Omit<QueryOptions<TDocumentType, TDataset, TProjectId>, 'query'> {
28
+ > extends WithResourceNameSupport<
29
+ Omit<QueryOptions<TDocumentType, TDataset, TProjectId>, 'query'>
30
+ > {
20
31
  documentType?: TDocumentType | TDocumentType[]
21
32
  /**
22
33
  * GROQ filter expression to apply to the query
@@ -232,16 +243,16 @@ export function usePaginatedDocuments<
232
243
  params = {},
233
244
  orderings,
234
245
  search,
235
- ...options
246
+ ...rawOptions
236
247
  }: PaginatedDocumentsOptions<TDocumentType, TDataset, TProjectId>): PaginatedDocumentsResponse<
237
248
  TDocumentType,
238
249
  TDataset,
239
250
  TProjectId
240
251
  > {
241
252
  useTrackHookUsage('usePaginatedDocuments')
242
- const instance = useSanityInstance(options)
253
+ const options = useNormalizedResourceOptions(rawOptions)
243
254
  const [pageIndex, setPageIndex] = useState(0)
244
- const key = JSON.stringify({filter, search, params, orderings, pageSize})
255
+ const key = JSON.stringify({filter, search, params, orderings, pageSize, ...options})
245
256
  // Reset pageIndex to 0 whenever any query parameter changes.
246
257
  const [prevKey, setPrevKey] = useState(key)
247
258
  if (prevKey !== key) {
@@ -303,9 +314,12 @@ export function usePaginatedDocuments<
303
314
  ...params,
304
315
  __types: documentTypes,
305
316
  __handle: {
306
- projectId: options.projectId ?? instance.config.projectId,
307
- dataset: options.dataset ?? instance.config.dataset,
308
- perspective: options.perspective ?? instance.config.perspective,
317
+ // keep projectId/dataset for backward compat until v4; resource is added
318
+ // intentionally so that hook consumers can resolve the correct resource
319
+ ...(options.resource && isDatasetResource(options.resource)
320
+ ? pickProperties(options.resource, ['projectId', 'dataset'])
321
+ : {}),
322
+ ...pickProperties(options, ['perspective', 'resource']),
309
323
  },
310
324
  },
311
325
  })
@@ -1,23 +1,16 @@
1
- import {
2
- type DatasetHandle,
3
- getPresence,
4
- isMediaLibraryResource,
5
- type UserPresence,
6
- } from '@sanity/sdk'
1
+ import {getPresence, isMediaLibraryResource, type UserPresence} from '@sanity/sdk'
7
2
  import {useCallback, useMemo, useSyncExternalStore} from 'react'
8
3
 
4
+ import {type ResourceHandle} from '../../config/handles'
9
5
  import {useSanityInstance} from '../context/useSanityInstance'
10
- import {
11
- useNormalizedResourceOptions,
12
- type WithResourceNameSupport,
13
- } from '../helpers/useNormalizedResourceOptions'
6
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
14
7
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
15
8
 
16
9
  /**
17
10
  * A hook for subscribing to presence information for the current project or Canvas.
18
11
  * @public
19
12
  */
20
- export function usePresence(options: WithResourceNameSupport<DatasetHandle> = {}): {
13
+ export function usePresence(options: ResourceHandle = {}): {
21
14
  locations: UserPresence[]
22
15
  } {
23
16
  const normalizedOptions = useNormalizedResourceOptions(options)
@@ -1,5 +1,4 @@
1
1
  import {
2
- type DocumentHandle,
3
2
  PREVIEW_PROJECTION,
4
3
  type PreviewQueryResult,
5
4
  type PreviewValue,
@@ -7,11 +6,9 @@ import {
7
6
  } from '@sanity/sdk'
8
7
  import {useMemo} from 'react'
9
8
 
9
+ import {type DocumentHandle} from '../../config/handles'
10
10
  import {useSanityInstance} from '../context/useSanityInstance'
11
- import {
12
- useNormalizedResourceOptions,
13
- type WithResourceNameSupport,
14
- } from '../helpers/useNormalizedResourceOptions'
11
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
15
12
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
16
13
  import {useDocumentProjection} from '../projection/useDocumentProjection'
17
14
 
@@ -19,7 +16,7 @@ import {useDocumentProjection} from '../projection/useDocumentProjection'
19
16
  * @public
20
17
  * @category Types
21
18
  */
22
- export interface useDocumentPreviewOptions extends WithResourceNameSupport<DocumentHandle> {
19
+ export interface useDocumentPreviewOptions extends DocumentHandle {
23
20
  /**
24
21
  * Optional ref object to track visibility. When provided, preview resolution
25
22
  * only occurs when the referenced element is visible in the viewport.
@@ -98,7 +95,7 @@ export function useDocumentPreview({
98
95
  ref,
99
96
  ...docHandle
100
97
  }: useDocumentPreviewOptions): useDocumentPreviewResults {
101
- const instance = useSanityInstance(docHandle)
98
+ const instance = useSanityInstance()
102
99
  trackHookUsage(instance, 'useDocumentPreview')
103
100
  const normalizedDocHandle = useNormalizedResourceOptions(docHandle)
104
101
 
@@ -1,13 +1,11 @@
1
- import {type DocumentHandle, getProjectionState, resolveProjection} from '@sanity/sdk'
1
+ import {getProjectionState, resolveProjection} from '@sanity/sdk'
2
2
  import {type SanityProjectionResult} from 'groq'
3
3
  import {useCallback, useMemo, useSyncExternalStore} from 'react'
4
4
  import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
5
5
 
6
+ import {type DocumentHandle} from '../../config/handles'
6
7
  import {useSanityInstance} from '../context/useSanityInstance'
7
- import {
8
- useNormalizedResourceOptions,
9
- type WithResourceNameSupport,
10
- } from '../helpers/useNormalizedResourceOptions'
8
+ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
11
9
  import {trackHookUsage} from '../helpers/useTrackHookUsage'
12
10
 
13
11
  /**
@@ -19,7 +17,7 @@ export interface useDocumentProjectionOptions<
19
17
  TDocumentType extends string = string,
20
18
  TDataset extends string = string,
21
19
  TProjectId extends string = string,
22
- > extends WithResourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
20
+ > extends DocumentHandle<TDocumentType, TDataset, TProjectId> {
23
21
  /** The GROQ projection string */
24
22
  projection: TProjection
25
23
  /** Optional parameters for the projection query */
@@ -181,7 +179,7 @@ export function useDocumentProjection<TData extends object>({
181
179
  projection,
182
180
  ...docHandle
183
181
  }: useDocumentProjectionOptions): useDocumentProjectionResults<TData> {
184
- const instance = useSanityInstance(docHandle)
182
+ const instance = useSanityInstance()
185
183
  trackHookUsage(instance, 'useDocumentProjection')
186
184
 
187
185
  // Normalize projection string to handle template literals with whitespace
@@ -0,0 +1,49 @@
1
+ import {type Project, type ProjectMember} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {useProject} from './useProject'
5
+
6
+ test('useProject — no args: members and features both included by default', () => {
7
+ expectTypeOf(useProject()).toEqualTypeOf<Project<true, true>>()
8
+ type Result = ReturnType<typeof useProject<true, true>>
9
+ expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[]>()
10
+ })
11
+
12
+ test('useProject — includeMembers: false drops members from the type', () => {
13
+ expectTypeOf(useProject({includeMembers: false})).toEqualTypeOf<Project<false, true>>()
14
+ })
15
+
16
+ test('useProject — includeFeatures: false drops features from the type', () => {
17
+ expectTypeOf(useProject({includeFeatures: false})).toEqualTypeOf<Project<true, false>>()
18
+ })
19
+
20
+ test('useProject — both flags true → both arrays present', () => {
21
+ expectTypeOf(useProject({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
22
+ Project<true, true>
23
+ >()
24
+ })
25
+
26
+ test('useProject — both flags false → bare base shape', () => {
27
+ expectTypeOf(useProject({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
28
+ Project<false, false>
29
+ >()
30
+ type Result = ReturnType<typeof useProject<false, false>>
31
+ expectTypeOf<Result['id']>().toEqualTypeOf<string>()
32
+ })
33
+
34
+ test('useProject — rejects non-boolean flag values', () => {
35
+ // @ts-expect-error — includeMembers must be a boolean
36
+ void useProject({includeMembers: 'yes'})
37
+ })
38
+
39
+ test('useProject — projectId alone does not change the data shape', () => {
40
+ expectTypeOf(useProject({projectId: 'p'})).toEqualTypeOf<Project<true, true>>()
41
+ })
42
+
43
+ test('useProject — non-literal boolean flag makes members optional', () => {
44
+ const includeMembers = false as boolean
45
+ expectTypeOf(useProject({includeMembers})).toEqualTypeOf<Project<boolean, true>>()
46
+ type Result = ReturnType<typeof useProject<boolean, true>>
47
+ expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
48
+ expectTypeOf<Pick<Result, 'members'>>().toEqualTypeOf<{members?: ProjectMember[]}>()
49
+ })
@@ -1,51 +1,43 @@
1
- import {
2
- getProjectState,
3
- type ProjectHandle,
4
- resolveProject,
5
- type SanityInstance,
6
- type SanityProject,
7
- type StateSource,
8
- } from '@sanity/sdk'
1
+ import {getProjectState, type Project, type ProjectOptions, resolveProject} from '@sanity/sdk'
9
2
  import {identity} from 'rxjs'
10
3
 
11
4
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
12
5
 
13
- type UseProject = {
14
- /**
15
- *
16
- * Returns metadata for a given project
17
- *
18
- * @category Projects
19
- * @param projectId - The ID of the project to retrieve metadata for
20
- * @returns The metadata for the project
21
- * @example
22
- * ```tsx
23
- * function ProjectMetadata({ projectId }: { projectId: string }) {
24
- * const project = useProject(projectId)
25
- *
26
- * return (
27
- * <figure style={{ backgroundColor: project.metadata.color || 'lavender'}}>
28
- * <h1>{project.displayName}</h1>
29
- * </figure>
30
- * )
31
- * }
32
- * ```
33
- */
34
- (projectHandle?: ProjectHandle): SanityProject
35
- }
36
-
37
6
  /**
7
+ * Returns metadata for a given project.
8
+ *
9
+ * @category Projects
10
+ * @param options - Configuration options
11
+ * @returns The metadata for the project. `members` is included only when
12
+ * `includeMembers: true`; `features` is included unless `includeFeatures: false`.
13
+ * @example
14
+ * ```tsx
15
+ * function ProjectMetadata({projectId}: {projectId: string}) {
16
+ * const project = useProject({projectId})
17
+ *
18
+ * return (
19
+ * <figure style={{backgroundColor: project.metadata.color || 'lavender'}}>
20
+ * <h1>{project.displayName}</h1>
21
+ * </figure>
22
+ * )
23
+ * }
24
+ * ```
25
+ * @example
26
+ * ```tsx
27
+ * const projectWithMembersAndFeatures = useProject({projectId})
28
+ * const projectWithMembers = useProject({projectId, includeMembers: true})
29
+ * const projectWithoutMembers = useProject({projectId, includeMembers: false})
30
+ * const projectWithoutFeatures = useProject({projectId, includeFeatures: false})
31
+ * ```
38
32
  * @public
39
33
  * @function
40
34
  */
41
- export const useProject: UseProject = createStateSourceHook({
42
- // remove `undefined` since we're suspending when that is the case
43
- getState: getProjectState as (
44
- instance: SanityInstance,
45
- projectHandle?: ProjectHandle,
46
- ) => StateSource<SanityProject>,
47
- shouldSuspend: (instance, projectHandle) =>
48
- getProjectState(instance, projectHandle).getCurrent() === undefined,
35
+ export const useProject = createStateSourceHook({
36
+ getState: getProjectState,
37
+ shouldSuspend: (instance, ...params) =>
38
+ getProjectState(instance, ...params).getCurrent() === undefined,
49
39
  suspender: resolveProject,
50
40
  getConfig: identity,
51
- })
41
+ }) as <IncludeMembers extends boolean = true, IncludeFeatures extends boolean = true>(
42
+ options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
43
+ ) => Project<IncludeMembers, IncludeFeatures>
@@ -0,0 +1,49 @@
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()).toEqualTypeOf<Project<false, true>[]>()
8
+ })
9
+
10
+ test('useProjects — includeMembers: true adds members to the type', () => {
11
+ expectTypeOf(useProjects({includeMembers: true})).toEqualTypeOf<Project<true, true>[]>()
12
+ type Result = ReturnType<typeof useProjects<true, true>>
13
+ expectTypeOf<Result[number]['members']>().toEqualTypeOf<ProjectMember[]>()
14
+ })
15
+
16
+ test('useProjects — includeFeatures: false drops features from the type', () => {
17
+ expectTypeOf(useProjects({includeFeatures: false})).toEqualTypeOf<Project<false, false>[]>()
18
+ })
19
+
20
+ test('useProjects — both flags true → both arrays present', () => {
21
+ expectTypeOf(useProjects({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
22
+ Project<true, true>[]
23
+ >()
24
+ })
25
+
26
+ test('useProjects — both flags false → bare base shape', () => {
27
+ expectTypeOf(useProjects({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
28
+ Project<false, false>[]
29
+ >()
30
+ type Result = ReturnType<typeof useProjects<false, false>>
31
+ expectTypeOf<Result[number]['id']>().toEqualTypeOf<string>()
32
+ })
33
+
34
+ test('useProjects — rejects non-boolean flag values', () => {
35
+ // @ts-expect-error — includeMembers must be a boolean
36
+ void useProjects({includeMembers: 'yes'})
37
+ })
38
+
39
+ test('useProjects — organizationId alone does not change the data shape', () => {
40
+ expectTypeOf(useProjects({organizationId: 'org_123'})).toEqualTypeOf<Project<false, true>[]>()
41
+ })
42
+
43
+ test('useProjects — non-literal boolean flag makes members optional', () => {
44
+ const includeMembers = false as boolean
45
+ expectTypeOf(useProjects({includeMembers})).toEqualTypeOf<Project<boolean, true>[]>()
46
+ type Result = ReturnType<typeof useProjects<boolean, true>>
47
+ expectTypeOf<Result[number]['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
48
+ expectTypeOf<Pick<Result[number], 'members'>>().toEqualTypeOf<{members?: ProjectMember[]}>()
49
+ })
@@ -1,5 +1,4 @@
1
- import {type SanityProject} from '@sanity/client'
2
- import {getProjectsState, resolveProjects, type SanityInstance, type StateSource} from '@sanity/sdk'
1
+ import {getProjectsState, type Project, type ProjectsOptions, resolveProjects} from '@sanity/sdk'
3
2
 
4
3
  import {createStateSourceHook} from '../helpers/createStateSourceHook'
5
4
 
@@ -7,24 +6,17 @@ import {createStateSourceHook} from '../helpers/createStateSourceHook'
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 An array of project metadata. `members` is included only when
19
+ * `includeMembers: true`; `features` is included unless `includeFeatures: false`.
28
20
  * @example
29
21
  * ```tsx
30
22
  * const projects = useProjects()
@@ -39,18 +31,20 @@ type UseProjects = <TIncludeMembers extends boolean = false>(options?: {
39
31
  * ```
40
32
  * @example
41
33
  * ```tsx
42
- * const projectsWithMembers = useProjects({ includeMembers: true })
43
- * const projectsWithoutMembers = useProjects({ includeMembers: false })
34
+ * const projects = useProjects()
35
+ * const projectsWithFeatures = useProjects()
36
+ * const projectsWithMembers = useProjects({includeMembers: true})
37
+ * const projectsWithoutMembers = useProjects({includeMembers: false})
38
+ * const projectsWithoutFeatures = useProjects({includeFeatures: false})
44
39
  * ```
45
40
  * @public
46
41
  * @function
47
42
  */
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,
43
+ export const useProjects = createStateSourceHook({
44
+ getState: getProjectsState,
45
+ shouldSuspend: (instance, ...params) =>
46
+ getProjectsState(instance, ...params).getCurrent() === undefined,
55
47
  suspender: resolveProjects,
56
- }) as UseProjects
48
+ }) as <IncludeMembers extends boolean = false, IncludeFeatures extends boolean = true>(
49
+ options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
50
+ ) => Project<IncludeMembers, IncludeFeatures>[]
@@ -152,7 +152,7 @@ export function useQuery(options: WithResourceNameSupport<QueryOptions>): {
152
152
  isPending: boolean
153
153
  } {
154
154
  // Implementation returns unknown, overloads define specifics
155
- const instance = useSanityInstance(options)
155
+ const instance = useSanityInstance()
156
156
  trackHookUsage(instance, 'useQuery')
157
157
 
158
158
  // Normalize options: resolve resourceName to resource and strip resourceName