@sanity/sdk-react 2.18.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 (54) hide show
  1. package/dist/index.d.ts +267 -112
  2. package/dist/index.js +131 -89
  3. package/dist/index.js.map +1 -1
  4. package/package.json +11 -9
  5. package/src/_exports/sdk-react.ts +14 -1
  6. package/src/components/auth/AuthBoundary.tsx +10 -5
  7. package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
  8. package/src/context/WorkbenchTokenRefresh.tsx +61 -0
  9. package/src/context/workbenchToken.ts +63 -0
  10. package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
  11. package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
  12. package/src/hooks/access/useCheckPermissions.ts +24 -0
  13. package/src/hooks/applications/useApplication.test-d.ts +30 -0
  14. package/src/hooks/applications/useApplication.ts +22 -0
  15. package/src/hooks/applications/useApplications.test-d.ts +31 -0
  16. package/src/hooks/applications/useApplications.ts +25 -0
  17. package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
  18. package/src/hooks/applications/useCreateUserApplication.ts +11 -0
  19. package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
  20. package/src/hooks/applications/useDeleteApplication.ts +11 -0
  21. package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
  22. package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
  23. package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
  24. package/src/hooks/applications/useUpdateApplication.ts +11 -0
  25. package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
  26. package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
  27. package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
  28. package/src/hooks/applications/useUserApplication.ts +14 -0
  29. package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
  30. package/src/hooks/applications/useUserApplications.ts +14 -0
  31. package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
  32. package/src/hooks/helpers/createFetcherHook.ts +69 -0
  33. package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
  34. package/src/hooks/helpers/createMutationHook.tsx +93 -0
  35. package/src/hooks/installations/useInstallation.test-d.ts +19 -0
  36. package/src/hooks/installations/useInstallation.ts +22 -0
  37. package/src/hooks/installations/useInstallations.test-d.ts +26 -0
  38. package/src/hooks/installations/useInstallations.ts +25 -0
  39. package/src/hooks/organizations/useOrganization.test-d.ts +19 -12
  40. package/src/hooks/organizations/useOrganization.test.ts +50 -52
  41. package/src/hooks/organizations/useOrganization.ts +13 -19
  42. package/src/hooks/organizations/useOrganizations.test-d.ts +26 -13
  43. package/src/hooks/organizations/useOrganizations.test.ts +49 -71
  44. package/src/hooks/organizations/useOrganizations.ts +14 -20
  45. package/src/hooks/projects/useProject.test-d.ts +18 -11
  46. package/src/hooks/projects/useProject.test.tsx +29 -23
  47. package/src/hooks/projects/useProject.ts +12 -16
  48. package/src/hooks/projects/useProjects.test-d.ts +22 -11
  49. package/src/hooks/projects/useProjects.test.ts +45 -98
  50. package/src/hooks/projects/useProjects.ts +16 -17
  51. package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -251
  52. package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
  53. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -128
  54. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -42
@@ -1,10 +1,5 @@
1
- import {
2
- createSanityInstance,
3
- getProjectState,
4
- type Project,
5
- resolveProject,
6
- type StateSource,
7
- } from '@sanity/sdk'
1
+ import {createSanityInstance, type Project, project, type StateSource} from '@sanity/sdk'
2
+ import {type FetcherSnapshot} from '@sanity/sdk/_internal'
8
3
  import {type ReactNode} from 'react'
9
4
  import {type Observable} from 'rxjs'
10
5
  import {beforeEach, describe, expect, it, vi} from 'vitest'
@@ -16,30 +11,41 @@ import {useProject} from './useProject'
16
11
 
17
12
  vi.mock('@sanity/sdk', async (importOriginal) => {
18
13
  const original = await importOriginal<typeof import('@sanity/sdk')>()
19
- return {...original, getProjectState: vi.fn(), resolveProject: vi.fn()}
14
+ return {...original, project: {getState: vi.fn(), resolveState: vi.fn()}}
20
15
  })
21
16
 
22
- const stateSource = (current: Project | undefined): StateSource<Project | undefined> =>
23
- ({
24
- getCurrent: vi.fn(() => current),
25
- subscribe: vi.fn(),
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(() => () => {}),
26
31
  get observable(): Observable<unknown> {
27
32
  throw new Error('Not implemented')
28
33
  },
29
- }) as unknown as StateSource<Project | undefined>
34
+ } as unknown as StateSource<FetcherSnapshot<Project>>
35
+ }
30
36
 
31
37
  const sanityInstance = expect.objectContaining({config: expect.any(Object)})
32
38
 
33
39
  describe('useProject', () => {
34
40
  beforeEach(() => {
35
41
  vi.clearAllMocks()
36
- vi.mocked(getProjectState).mockReturnValue(stateSource({id: 'p'} as unknown as Project))
42
+ vi.mocked(project.getState).mockReturnValue(stateSource({id: 'p'} as unknown as Project))
37
43
  })
38
44
 
39
45
  it('resolves the projectId from the instance config resource', () => {
40
46
  // test-utils wraps with ResourceProvider projectId="test" dataset="test".
41
47
  renderHook(() => useProject())
42
- expect(getProjectState).toHaveBeenCalledWith(
48
+ expect(project.getState).toHaveBeenCalledWith(
43
49
  sanityInstance,
44
50
  expect.objectContaining({projectId: 'test'}),
45
51
  )
@@ -47,7 +53,7 @@ describe('useProject', () => {
47
53
 
48
54
  it('lets an explicit projectId override the ambient resource', () => {
49
55
  renderHook(() => useProject({projectId: 'explicit-project'}))
50
- expect(getProjectState).toHaveBeenCalledWith(
56
+ expect(project.getState).toHaveBeenCalledWith(
51
57
  sanityInstance,
52
58
  expect.objectContaining({projectId: 'explicit-project'}),
53
59
  )
@@ -64,7 +70,7 @@ describe('useProject', () => {
64
70
  </ResourceProvider>
65
71
  ),
66
72
  })
67
- expect(getProjectState).toHaveBeenCalledWith(
73
+ expect(project.getState).toHaveBeenCalledWith(
68
74
  sanityInstance,
69
75
  expect.objectContaining({projectId: 'resource-project'}),
70
76
  )
@@ -80,7 +86,7 @@ describe('useProject', () => {
80
86
  })
81
87
  // A dataset-less config can't form a DatasetResource; the projectId is carried
82
88
  // via ProjectContext and injected so project-scoped reads still resolve it.
83
- expect(getProjectState).toHaveBeenCalledWith(
89
+ expect(project.getState).toHaveBeenCalledWith(
84
90
  sanityInstance,
85
91
  expect.objectContaining({projectId: 'config-project'}),
86
92
  )
@@ -99,16 +105,16 @@ describe('useProject', () => {
99
105
  </SanityInstanceContext.Provider>
100
106
  ),
101
107
  })
102
- expect(getProjectState).toHaveBeenCalledWith(
108
+ expect(project.getState).toHaveBeenCalledWith(
103
109
  emptyInstance,
104
110
  expect.objectContaining({projectId: 'bare-project'}),
105
111
  )
106
112
  })
107
113
 
108
- it('suspends via resolveProject until project data is available', () => {
109
- vi.mocked(getProjectState).mockReturnValue(stateSource(undefined))
110
- vi.mocked(resolveProject).mockReturnValue(new Promise(() => {}))
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(() => {}))
111
117
  renderHook(() => useProject())
112
- expect(resolveProject).toHaveBeenCalled()
118
+ expect(project.resolveState).toHaveBeenCalled()
113
119
  })
114
120
  })
@@ -1,26 +1,22 @@
1
- import {getProjectState, type Project, type ProjectOptions, resolveProject} from '@sanity/sdk'
1
+ import {type Project, project, type ProjectOptions} from '@sanity/sdk'
2
2
 
3
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
4
4
  import {useResolvedProjectId} from '../helpers/useResolvedProjectId'
5
5
 
6
- const useProjectBase = createStateSourceHook({
7
- getState: getProjectState,
8
- shouldSuspend: (instance, ...params) =>
9
- getProjectState(instance, ...params).getCurrent() === undefined,
10
- suspender: resolveProject,
11
- })
6
+ const useProjectBase = createFetcherHook(project)
12
7
 
13
8
  /**
14
9
  * Returns metadata for a given project.
15
10
  *
16
11
  * @category Projects
17
12
  * @param options - Configuration options
18
- * @returns The metadata for the project. `members` is included only when
19
- * `includeMembers: true`; `features` is included unless `includeFeatures: false`.
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`.
20
16
  * @example
21
17
  * ```tsx
22
18
  * function ProjectMetadata({projectId}: {projectId: string}) {
23
- * const project = useProject({projectId})
19
+ * const {data: project} = useProject({projectId})
24
20
  *
25
21
  * return (
26
22
  * <figure style={{backgroundColor: project.metadata.color || 'lavender'}}>
@@ -31,10 +27,10 @@ const useProjectBase = createStateSourceHook({
31
27
  * ```
32
28
  * @example
33
29
  * ```tsx
34
- * const projectWithMembersAndFeatures = useProject({projectId})
35
- * const projectWithMembers = useProject({projectId, includeMembers: true})
36
- * const projectWithoutMembers = useProject({projectId, includeMembers: false})
37
- * const projectWithoutFeatures = useProject({projectId, includeFeatures: false})
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})
38
34
  * ```
39
35
  * @remarks
40
36
  * The `projectId` is resolved in order from:
@@ -50,4 +46,4 @@ export const useProject = ((options?: ProjectOptions<boolean, boolean>) => {
50
46
  return useProjectBase(projectId ? {...options, projectId} : options)
51
47
  }) as <IncludeMembers extends boolean = true, IncludeFeatures extends boolean = true>(
52
48
  options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
53
- ) => Project<IncludeMembers, IncludeFeatures>
49
+ ) => FetcherHookResult<Project<IncludeMembers, IncludeFeatures>>
@@ -4,31 +4,38 @@ import {expectTypeOf, test} from 'vitest'
4
4
  import {useProjects} from './useProjects'
5
5
 
6
6
  test('useProjects — no args: features included, members omitted', () => {
7
- expectTypeOf(useProjects()).toEqualTypeOf<Project<false, true>[]>()
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>[]>>()
8
15
  })
9
16
 
10
17
  test('useProjects — includeMembers: true adds members to the type', () => {
11
- expectTypeOf(useProjects({includeMembers: true})).toEqualTypeOf<Project<true, true>[]>()
18
+ expectTypeOf(useProjects({includeMembers: true}).data).toEqualTypeOf<Project<true, true>[]>()
12
19
  type Result = ReturnType<typeof useProjects<true, true>>
13
- expectTypeOf<Result[number]['members']>().toEqualTypeOf<ProjectMember[]>()
20
+ expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<ProjectMember[]>()
14
21
  })
15
22
 
16
23
  test('useProjects — includeFeatures: false drops features from the type', () => {
17
- expectTypeOf(useProjects({includeFeatures: false})).toEqualTypeOf<Project<false, false>[]>()
24
+ expectTypeOf(useProjects({includeFeatures: false}).data).toEqualTypeOf<Project<false, false>[]>()
18
25
  })
19
26
 
20
27
  test('useProjects — both flags true → both arrays present', () => {
21
- expectTypeOf(useProjects({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
28
+ expectTypeOf(useProjects({includeMembers: true, includeFeatures: true}).data).toEqualTypeOf<
22
29
  Project<true, true>[]
23
30
  >()
24
31
  })
25
32
 
26
33
  test('useProjects — both flags false → bare base shape', () => {
27
- expectTypeOf(useProjects({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
34
+ expectTypeOf(useProjects({includeMembers: false, includeFeatures: false}).data).toEqualTypeOf<
28
35
  Project<false, false>[]
29
36
  >()
30
37
  type Result = ReturnType<typeof useProjects<false, false>>
31
- expectTypeOf<Result[number]['id']>().toEqualTypeOf<string>()
38
+ expectTypeOf<Result['data'][number]['id']>().toEqualTypeOf<string>()
32
39
  })
33
40
 
34
41
  test('useProjects — rejects non-boolean flag values', () => {
@@ -37,13 +44,17 @@ test('useProjects — rejects non-boolean flag values', () => {
37
44
  })
38
45
 
39
46
  test('useProjects — organizationId alone does not change the data shape', () => {
40
- expectTypeOf(useProjects({organizationId: 'org_123'})).toEqualTypeOf<Project<false, true>[]>()
47
+ expectTypeOf(useProjects({organizationId: 'org_123'}).data).toEqualTypeOf<
48
+ Project<false, true>[]
49
+ >()
41
50
  })
42
51
 
43
52
  test('useProjects — non-literal boolean flag makes members optional', () => {
44
53
  const includeMembers = false as boolean
45
- expectTypeOf(useProjects({includeMembers})).toEqualTypeOf<Project<boolean, true>[]>()
54
+ expectTypeOf(useProjects({includeMembers}).data).toEqualTypeOf<Project<boolean, true>[]>()
46
55
  type Result = ReturnType<typeof useProjects<boolean, true>>
47
- expectTypeOf<Result[number]['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
48
- expectTypeOf<Pick<Result[number], 'members'>>().toEqualTypeOf<{members?: ProjectMember[]}>()
56
+ expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
57
+ expectTypeOf<Pick<Result['data'][number], 'members'>>().toEqualTypeOf<{
58
+ members?: ProjectMember[]
59
+ }>()
49
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,6 +1,6 @@
1
- import {getProjectsState, type Project, type ProjectsOptions, resolveProjects} from '@sanity/sdk'
1
+ import {type Project, projects, type ProjectsOptions} from '@sanity/sdk'
2
2
 
3
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
4
4
 
5
5
  /**
6
6
  * @public
@@ -15,11 +15,12 @@ export type ProjectWithoutMembers = Project
15
15
  *
16
16
  * @category Projects
17
17
  * @param options - Configuration options
18
- * @returns An array of project metadata. `members` is included only when
19
- * `includeMembers: true`; `features` is included unless `includeFeatures: false`.
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`.
20
21
  * @example
21
22
  * ```tsx
22
- * const projects = useProjects()
23
+ * const {data: projects} = useProjects()
23
24
  *
24
25
  * return (
25
26
  * <select>
@@ -31,20 +32,18 @@ export type ProjectWithoutMembers = Project
31
32
  * ```
32
33
  * @example
33
34
  * ```tsx
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})
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})
39
40
  * ```
40
41
  * @public
41
42
  * @function
42
43
  */
43
- export const useProjects = createStateSourceHook({
44
- getState: getProjectsState,
45
- shouldSuspend: (instance, ...params) =>
46
- getProjectsState(instance, ...params).getCurrent() === undefined,
47
- suspender: resolveProjects,
48
- }) as <IncludeMembers extends boolean = false, IncludeFeatures extends boolean = true>(
44
+ export const useProjects = createFetcherHook(projects) as <
45
+ IncludeMembers extends boolean = false,
46
+ IncludeFeatures extends boolean = true,
47
+ >(
49
48
  options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
50
- ) => Project<IncludeMembers, IncludeFeatures>[]
49
+ ) => FetcherHookResult<Project<IncludeMembers, IncludeFeatures>[]>