@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
@@ -4,37 +4,44 @@ import {expectTypeOf, test} from 'vitest'
4
4
  import {useOrganization} from './useOrganization'
5
5
 
6
6
  test('useOrganization — no flags: members and features both omitted', () => {
7
- expectTypeOf(useOrganization({organizationId: 'org_1'})).toEqualTypeOf<
7
+ expectTypeOf(useOrganization({organizationId: 'org_1'}).data).toEqualTypeOf<
8
8
  Organization<false, false>
9
9
  >()
10
10
  })
11
11
 
12
+ test('useOrganization — returns the FetcherHookResult envelope', () => {
13
+ const result = useOrganization({organizationId: 'org_1'})
14
+ expectTypeOf(result.isFetching).toEqualTypeOf<boolean>()
15
+ expectTypeOf(result.error).toEqualTypeOf<unknown>()
16
+ expectTypeOf(result.refetch).toEqualTypeOf<() => Promise<Organization<false, false>>>()
17
+ })
18
+
12
19
  test('useOrganization — includeMembers: true adds members to the type', () => {
13
- expectTypeOf(useOrganization({organizationId: 'org_1', includeMembers: true})).toEqualTypeOf<
20
+ expectTypeOf(useOrganization({organizationId: 'org_1', includeMembers: true}).data).toEqualTypeOf<
14
21
  Organization<true, false>
15
22
  >()
16
23
  type Result = ReturnType<typeof useOrganization<true, false>>
17
- expectTypeOf<Result['members']>().toEqualTypeOf<OrganizationMember[]>()
24
+ expectTypeOf<Result['data']['members']>().toEqualTypeOf<OrganizationMember[]>()
18
25
  })
19
26
 
20
27
  test('useOrganization — includeFeatures: true adds features to the type', () => {
21
- expectTypeOf(useOrganization({organizationId: 'org_1', includeFeatures: true})).toEqualTypeOf<
22
- Organization<false, true>
23
- >()
28
+ expectTypeOf(
29
+ useOrganization({organizationId: 'org_1', includeFeatures: true}).data,
30
+ ).toEqualTypeOf<Organization<false, true>>()
24
31
  })
25
32
 
26
33
  test('useOrganization — both flags true → both arrays present', () => {
27
34
  expectTypeOf(
28
- useOrganization({organizationId: 'org_1', includeMembers: true, includeFeatures: true}),
35
+ useOrganization({organizationId: 'org_1', includeMembers: true, includeFeatures: true}).data,
29
36
  ).toEqualTypeOf<Organization<true, true>>()
30
37
  })
31
38
 
32
39
  test('useOrganization — both flags false → bare base shape', () => {
33
40
  expectTypeOf(
34
- useOrganization({organizationId: 'org_1', includeMembers: false, includeFeatures: false}),
41
+ useOrganization({organizationId: 'org_1', includeMembers: false, includeFeatures: false}).data,
35
42
  ).toEqualTypeOf<Organization<false, false>>()
36
43
  type Result = ReturnType<typeof useOrganization<false, false>>
37
- expectTypeOf<Result['id']>().toEqualTypeOf<string>()
44
+ expectTypeOf<Result['data']['id']>().toEqualTypeOf<string>()
38
45
  })
39
46
 
40
47
  test('useOrganization — rejects non-boolean flag values', () => {
@@ -44,10 +51,10 @@ test('useOrganization — rejects non-boolean flag values', () => {
44
51
 
45
52
  test('useOrganization — non-literal boolean flag makes members optional', () => {
46
53
  const includeMembers = false as boolean
47
- expectTypeOf(useOrganization({organizationId: 'org_1', includeMembers})).toEqualTypeOf<
54
+ expectTypeOf(useOrganization({organizationId: 'org_1', includeMembers}).data).toEqualTypeOf<
48
55
  Organization<boolean, false>
49
56
  >()
50
57
  type Result = ReturnType<typeof useOrganization<boolean, false>>
51
- expectTypeOf<Result['members']>().toEqualTypeOf<OrganizationMember[] | undefined>()
52
- expectTypeOf<Pick<Result, 'members'>>().toEqualTypeOf<{members?: OrganizationMember[]}>()
58
+ expectTypeOf<Result['data']['members']>().toEqualTypeOf<OrganizationMember[] | undefined>()
59
+ expectTypeOf<Pick<Result['data'], 'members'>>().toEqualTypeOf<{members?: OrganizationMember[]}>()
53
60
  })
@@ -1,65 +1,63 @@
1
- import {getOrganizationState, type OrganizationOptions, type SanityInstance} from '@sanity/sdk'
1
+ import {type Organization, organization, 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 {useOrganization} from './useOrganization'
5
8
 
6
- vi.mock('@sanity/sdk', () => ({
7
- getOrganizationState: vi.fn(() => ({
8
- getCurrent: vi.fn(() => undefined),
9
- })),
10
- resolveOrganization: vi.fn(),
11
- }))
12
- vi.mock('../helpers/createStateSourceHook', () => ({
13
- createStateSourceHook: vi.fn(),
14
- }))
9
+ vi.mock('@sanity/sdk', async (importOriginal) => {
10
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
11
+ return {...original, organization: {getState: vi.fn(), resolveState: vi.fn(), refetch: vi.fn()}}
12
+ })
13
+
14
+ const stateSource = (
15
+ current: Organization | undefined,
16
+ ): StateSource<FetcherSnapshot<Organization>> => {
17
+ const snapshot = current
18
+ ? {status: 'success', data: current, error: undefined, isFetching: false, dataUpdatedAt: 1}
19
+ : {
20
+ status: 'pending',
21
+ data: undefined,
22
+ error: undefined,
23
+ isFetching: true,
24
+ dataUpdatedAt: undefined,
25
+ }
26
+ return {
27
+ getCurrent: vi.fn(() => snapshot),
28
+ subscribe: vi.fn(() => () => {}),
29
+ get observable(): Observable<unknown> {
30
+ throw new Error('Not implemented')
31
+ },
32
+ } as unknown as StateSource<FetcherSnapshot<Organization>>
33
+ }
34
+
35
+ const sanityInstance = expect.objectContaining({config: expect.any(Object)})
15
36
 
16
37
  describe('useOrganization', () => {
17
38
  beforeEach(() => {
18
- vi.resetModules()
19
- vi.mock('@sanity/sdk', () => ({
20
- getOrganizationState: vi.fn(() => ({
21
- getCurrent: vi.fn(() => undefined),
22
- })),
23
- resolveOrganization: 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('./useOrganization')
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
- }),
39
+ vi.clearAllMocks()
40
+ vi.mocked(organization.getState).mockReturnValue(
41
+ stateSource({id: 'org_1'} as unknown as Organization),
40
42
  )
41
43
  })
42
44
 
43
- it('shouldSuspend should call getOrganizationState and getCurrent', async () => {
44
- await import('./useOrganization')
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
- const mockOptions: OrganizationOptions = {organizationId: 'org_1'}
53
-
54
- const result = shouldSuspend(mockInstance, mockOptions)
55
-
56
- const mockGetOrganizationState = getOrganizationState as ReturnType<typeof vi.fn>
57
- expect(mockGetOrganizationState).toHaveBeenCalledWith(mockInstance, mockOptions)
45
+ it('reads the organization fetcher with the passed options', () => {
46
+ renderHook(() => useOrganization({organizationId: 'org_1'}))
47
+ expect(organization.getState).toHaveBeenCalledWith(sanityInstance, {organizationId: 'org_1'})
48
+ })
58
49
 
59
- expect(mockGetOrganizationState.mock.results.length).toBeGreaterThan(0)
60
- const getOrganizationStateMockResult = mockGetOrganizationState.mock.results[0].value
61
- expect(getOrganizationStateMockResult.getCurrent).toHaveBeenCalled()
50
+ it('returns the fetcher data in the result envelope', () => {
51
+ const {result} = renderHook(() => useOrganization({organizationId: 'org_1'}))
52
+ expect(result.current.data).toEqual({id: 'org_1'})
53
+ expect(result.current.isFetching).toBe(false)
54
+ expect(typeof result.current.refetch).toBe('function')
55
+ })
62
56
 
63
- expect(result).toBe(true)
57
+ it('suspends via the organization fetcher until data is available', () => {
58
+ vi.mocked(organization.getState).mockReturnValue(stateSource(undefined))
59
+ vi.mocked(organization.resolveState).mockReturnValue(new Promise(() => {}))
60
+ renderHook(() => useOrganization({organizationId: 'org_1'}))
61
+ expect(organization.resolveState).toHaveBeenCalled()
64
62
  })
65
63
  })
@@ -1,40 +1,34 @@
1
- import {
2
- getOrganizationState,
3
- type Organization,
4
- type OrganizationOptions,
5
- resolveOrganization,
6
- } from '@sanity/sdk'
1
+ import {type Organization, organization, type OrganizationOptions} from '@sanity/sdk'
7
2
 
8
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
9
4
 
10
5
  /**
11
6
  * Returns metadata for a given organisation.
12
7
  *
13
8
  * @category Organizations
14
9
  * @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`.
10
+ * @returns A {@link FetcherHookResult} whose `data` is the metadata for the
11
+ * organisation. `members` is included only when `includeMembers: true`;
12
+ * `features` is included only when `includeFeatures: true`.
17
13
  * @example
18
14
  * ```tsx
19
15
  * function OrganizationName({organizationId}: {organizationId: string}) {
20
- * const organization = useOrganization({organizationId})
16
+ * const {data: organization} = useOrganization({organizationId})
21
17
  *
22
18
  * return <h1>{organization.name}</h1>
23
19
  * }
24
20
  * ```
25
21
  * @example
26
22
  * ```tsx
27
- * const organizationWithMembers = useOrganization({organizationId, includeMembers: true})
28
- * const organizationWithFeatures = useOrganization({organizationId, includeFeatures: true})
23
+ * const {data: organizationWithMembers} = useOrganization({organizationId, includeMembers: true})
24
+ * const {data: organizationWithFeatures} = useOrganization({organizationId, includeFeatures: true})
29
25
  * ```
30
26
  * @public
31
27
  * @function
32
28
  */
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>(
29
+ export const useOrganization = createFetcherHook(organization) as <
30
+ IncludeMembers extends boolean = false,
31
+ IncludeFeatures extends boolean = false,
32
+ >(
39
33
  options: OrganizationOptions<IncludeMembers, IncludeFeatures>,
40
- ) => Organization<IncludeMembers, IncludeFeatures>
34
+ ) => FetcherHookResult<Organization<IncludeMembers, IncludeFeatures>>
@@ -4,33 +4,42 @@ import {expectTypeOf, test} from 'vitest'
4
4
  import {useOrganizations} from './useOrganizations'
5
5
 
6
6
  test('useOrganizations — no args: members and features both omitted', () => {
7
- expectTypeOf(useOrganizations()).toEqualTypeOf<Organizations<false, false>>()
7
+ expectTypeOf(useOrganizations().data).toEqualTypeOf<Organizations<false, false>>()
8
+ })
9
+
10
+ test('useOrganizations — returns the FetcherHookResult envelope', () => {
11
+ const result = useOrganizations()
12
+ expectTypeOf(result.isFetching).toEqualTypeOf<boolean>()
13
+ expectTypeOf(result.error).toEqualTypeOf<unknown>()
14
+ expectTypeOf(result.refetch).toEqualTypeOf<() => Promise<Organizations<false, false>>>()
8
15
  })
9
16
 
10
17
  test('useOrganizations — includeMembers: true adds members to the type', () => {
11
- expectTypeOf(useOrganizations({includeMembers: true})).toEqualTypeOf<Organizations<true, false>>()
18
+ expectTypeOf(useOrganizations({includeMembers: true}).data).toEqualTypeOf<
19
+ Organizations<true, false>
20
+ >()
12
21
  type Result = ReturnType<typeof useOrganizations<true, false>>
13
- expectTypeOf<Result[number]['members']>().toEqualTypeOf<OrganizationMember[]>()
22
+ expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<OrganizationMember[]>()
14
23
  })
15
24
 
16
25
  test('useOrganizations — includeFeatures: true adds features to the type', () => {
17
- expectTypeOf(useOrganizations({includeFeatures: true})).toEqualTypeOf<
26
+ expectTypeOf(useOrganizations({includeFeatures: true}).data).toEqualTypeOf<
18
27
  Organizations<false, true>
19
28
  >()
20
29
  })
21
30
 
22
31
  test('useOrganizations — both flags true → both arrays present', () => {
23
- expectTypeOf(useOrganizations({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
32
+ expectTypeOf(useOrganizations({includeMembers: true, includeFeatures: true}).data).toEqualTypeOf<
24
33
  Organizations<true, true>
25
34
  >()
26
35
  })
27
36
 
28
37
  test('useOrganizations — both flags false → bare base shape', () => {
29
- expectTypeOf(useOrganizations({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
30
- Organizations<false, false>
31
- >()
38
+ expectTypeOf(
39
+ useOrganizations({includeMembers: false, includeFeatures: false}).data,
40
+ ).toEqualTypeOf<Organizations<false, false>>()
32
41
  type Result = ReturnType<typeof useOrganizations<false, false>>
33
- expectTypeOf<Result[number]['id']>().toEqualTypeOf<string>()
42
+ expectTypeOf<Result['data'][number]['id']>().toEqualTypeOf<string>()
34
43
  })
35
44
 
36
45
  test('useOrganizations — rejects non-boolean flag values', () => {
@@ -39,17 +48,21 @@ test('useOrganizations — rejects non-boolean flag values', () => {
39
48
  })
40
49
 
41
50
  test('useOrganizations — includeImplicitMemberships does not change the data shape', () => {
42
- expectTypeOf(useOrganizations({includeImplicitMemberships: true})).toEqualTypeOf<
51
+ expectTypeOf(useOrganizations({includeImplicitMemberships: true}).data).toEqualTypeOf<
43
52
  Organizations<false, false>
44
53
  >()
45
54
  })
46
55
 
47
56
  test('useOrganizations — non-literal boolean flag makes members optional', () => {
48
57
  const includeMembers = false as boolean
49
- expectTypeOf(useOrganizations({includeMembers})).toEqualTypeOf<Organizations<boolean, false>>()
58
+ expectTypeOf(useOrganizations({includeMembers}).data).toEqualTypeOf<
59
+ Organizations<boolean, false>
60
+ >()
50
61
  type Result = ReturnType<typeof useOrganizations<boolean, false>>
51
- expectTypeOf<Result[number]['members']>().toEqualTypeOf<OrganizationMember[] | undefined>()
52
- expectTypeOf<Pick<Result[number], 'members'>>().toEqualTypeOf<{
62
+ expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<
63
+ OrganizationMember[] | undefined
64
+ >()
65
+ expectTypeOf<Pick<Result['data'][number], 'members'>>().toEqualTypeOf<{
53
66
  members?: OrganizationMember[]
54
67
  }>()
55
68
  })
@@ -1,85 +1,63 @@
1
- import {getOrganizationsState, type SanityInstance} from '@sanity/sdk'
1
+ import {type Organizations, organizations, 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 {useOrganizations} from './useOrganizations'
5
8
 
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
- }))
9
+ vi.mock('@sanity/sdk', async (importOriginal) => {
10
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
11
+ return {...original, organizations: {getState: vi.fn(), resolveState: vi.fn(), refetch: vi.fn()}}
12
+ })
13
+
14
+ const stateSource = (
15
+ current: Organizations | undefined,
16
+ ): StateSource<FetcherSnapshot<Organizations>> => {
17
+ const snapshot = current
18
+ ? {status: 'success', data: current, error: undefined, isFetching: false, dataUpdatedAt: 1}
19
+ : {
20
+ status: 'pending',
21
+ data: undefined,
22
+ error: undefined,
23
+ isFetching: true,
24
+ dataUpdatedAt: undefined,
25
+ }
26
+ return {
27
+ getCurrent: vi.fn(() => snapshot),
28
+ subscribe: vi.fn(() => () => {}),
29
+ get observable(): Observable<unknown> {
30
+ throw new Error('Not implemented')
31
+ },
32
+ } as unknown as StateSource<FetcherSnapshot<Organizations>>
33
+ }
34
+
35
+ const sanityInstance = expect.objectContaining({config: expect.any(Object)})
15
36
 
16
37
  describe('useOrganizations', () => {
17
38
  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
- }),
39
+ vi.clearAllMocks()
40
+ vi.mocked(organizations.getState).mockReturnValue(
41
+ stateSource([{id: 'org_1'}] as unknown as Organizations),
40
42
  )
41
43
  })
42
44
 
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)
45
+ it('reads the organizations fetcher with the passed options', () => {
46
+ renderHook(() => useOrganizations({includeMembers: true}))
47
+ expect(organizations.getState).toHaveBeenCalledWith(sanityInstance, {includeMembers: true})
63
48
  })
64
49
 
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
50
+ it('returns the fetcher data in the result envelope', () => {
51
+ const {result} = renderHook(() => useOrganizations())
52
+ expect(result.current.data).toEqual([{id: 'org_1'}])
53
+ expect(result.current.isFetching).toBe(false)
54
+ expect(typeof result.current.refetch).toBe('function')
55
+ })
73
56
 
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()
57
+ it('suspends via the organizations fetcher until data is available', () => {
58
+ vi.mocked(organizations.getState).mockReturnValue(stateSource(undefined))
59
+ vi.mocked(organizations.resolveState).mockReturnValue(new Promise(() => {}))
60
+ renderHook(() => useOrganizations())
61
+ expect(organizations.resolveState).toHaveBeenCalled()
84
62
  })
85
63
  })
@@ -1,22 +1,18 @@
1
- import {
2
- getOrganizationsState,
3
- type Organizations,
4
- type OrganizationsOptions,
5
- resolveOrganizations,
6
- } from '@sanity/sdk'
1
+ import {type Organizations, organizations, type OrganizationsOptions} from '@sanity/sdk'
7
2
 
8
- import {createStateSourceHook} from '../helpers/createStateSourceHook'
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
9
4
 
10
5
  /**
11
6
  * Returns metadata for each organisation the current user has access to.
12
7
  *
13
8
  * @category Organizations
14
9
  * @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`.
10
+ * @returns A {@link FetcherHookResult} whose `data` is an array of organisation
11
+ * metadata. `members` is included only when `includeMembers: true`; `features`
12
+ * is included only when `includeFeatures: true`.
17
13
  * @example
18
14
  * ```tsx
19
- * const organizations = useOrganizations()
15
+ * const {data: organizations} = useOrganizations()
20
16
  *
21
17
  * return (
22
18
  * <select>
@@ -28,18 +24,16 @@ import {createStateSourceHook} from '../helpers/createStateSourceHook'
28
24
  * ```
29
25
  * @example
30
26
  * ```tsx
31
- * const organizationsWithMembers = useOrganizations({includeMembers: true})
32
- * const organizationsWithFeatures = useOrganizations({includeFeatures: true})
33
- * const organizationsIncludingImplicit = useOrganizations({includeImplicitMemberships: true})
27
+ * const {data: organizationsWithMembers} = useOrganizations({includeMembers: true})
28
+ * const {data: organizationsWithFeatures} = useOrganizations({includeFeatures: true})
29
+ * const {data: organizationsIncludingImplicit} = useOrganizations({includeImplicitMemberships: true})
34
30
  * ```
35
31
  * @public
36
32
  * @function
37
33
  */
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>(
34
+ export const useOrganizations = createFetcherHook(organizations) as <
35
+ IncludeMembers extends boolean = false,
36
+ IncludeFeatures extends boolean = false,
37
+ >(
44
38
  options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
45
- ) => Organizations<IncludeMembers, IncludeFeatures>
39
+ ) => FetcherHookResult<Organizations<IncludeMembers, IncludeFeatures>>
@@ -4,31 +4,38 @@ import {expectTypeOf, test} from 'vitest'
4
4
  import {useProject} from './useProject'
5
5
 
6
6
  test('useProject — no args: members and features both included by default', () => {
7
- expectTypeOf(useProject()).toEqualTypeOf<Project<true, true>>()
7
+ expectTypeOf(useProject().data).toEqualTypeOf<Project<true, true>>()
8
8
  type Result = ReturnType<typeof useProject<true, true>>
9
- expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[]>()
9
+ expectTypeOf<Result['data']['members']>().toEqualTypeOf<ProjectMember[]>()
10
+ })
11
+
12
+ test('useProject — returns the FetcherHookResult envelope', () => {
13
+ const result = useProject()
14
+ expectTypeOf(result.isFetching).toEqualTypeOf<boolean>()
15
+ expectTypeOf(result.error).toEqualTypeOf<unknown>()
16
+ expectTypeOf(result.refetch).toEqualTypeOf<() => Promise<Project<true, true>>>()
10
17
  })
11
18
 
12
19
  test('useProject — includeMembers: false drops members from the type', () => {
13
- expectTypeOf(useProject({includeMembers: false})).toEqualTypeOf<Project<false, true>>()
20
+ expectTypeOf(useProject({includeMembers: false}).data).toEqualTypeOf<Project<false, true>>()
14
21
  })
15
22
 
16
23
  test('useProject — includeFeatures: false drops features from the type', () => {
17
- expectTypeOf(useProject({includeFeatures: false})).toEqualTypeOf<Project<true, false>>()
24
+ expectTypeOf(useProject({includeFeatures: false}).data).toEqualTypeOf<Project<true, false>>()
18
25
  })
19
26
 
20
27
  test('useProject — both flags true → both arrays present', () => {
21
- expectTypeOf(useProject({includeMembers: true, includeFeatures: true})).toEqualTypeOf<
28
+ expectTypeOf(useProject({includeMembers: true, includeFeatures: true}).data).toEqualTypeOf<
22
29
  Project<true, true>
23
30
  >()
24
31
  })
25
32
 
26
33
  test('useProject — both flags false → bare base shape', () => {
27
- expectTypeOf(useProject({includeMembers: false, includeFeatures: false})).toEqualTypeOf<
34
+ expectTypeOf(useProject({includeMembers: false, includeFeatures: false}).data).toEqualTypeOf<
28
35
  Project<false, false>
29
36
  >()
30
37
  type Result = ReturnType<typeof useProject<false, false>>
31
- expectTypeOf<Result['id']>().toEqualTypeOf<string>()
38
+ expectTypeOf<Result['data']['id']>().toEqualTypeOf<string>()
32
39
  })
33
40
 
34
41
  test('useProject — rejects non-boolean flag values', () => {
@@ -37,13 +44,13 @@ test('useProject — rejects non-boolean flag values', () => {
37
44
  })
38
45
 
39
46
  test('useProject — projectId alone does not change the data shape', () => {
40
- expectTypeOf(useProject({projectId: 'p'})).toEqualTypeOf<Project<true, true>>()
47
+ expectTypeOf(useProject({projectId: 'p'}).data).toEqualTypeOf<Project<true, true>>()
41
48
  })
42
49
 
43
50
  test('useProject — non-literal boolean flag makes members optional', () => {
44
51
  const includeMembers = false as boolean
45
- expectTypeOf(useProject({includeMembers})).toEqualTypeOf<Project<boolean, true>>()
52
+ expectTypeOf(useProject({includeMembers}).data).toEqualTypeOf<Project<boolean, true>>()
46
53
  type Result = ReturnType<typeof useProject<boolean, true>>
47
- expectTypeOf<Result['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
48
- expectTypeOf<Pick<Result, 'members'>>().toEqualTypeOf<{members?: ProjectMember[]}>()
54
+ expectTypeOf<Result['data']['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
55
+ expectTypeOf<Pick<Result['data'], 'members'>>().toEqualTypeOf<{members?: ProjectMember[]}>()
49
56
  })