@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,26 @@
1
+ import {type Installation, type InstallationAccess, type InstallationsResponse} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {type FetcherHookResult} from '../helpers/createFetcherHook'
5
+ import {useInstallations} from './useInstallations'
6
+
7
+ test('useInstallations — no include: base items, no gated fields', () => {
8
+ const result = useInstallations({organizationId: 'org_1'})
9
+ expectTypeOf(result).toEqualTypeOf<FetcherHookResult<InstallationsResponse<never>>>()
10
+ type Item = (typeof result.data.data)[number]
11
+ expectTypeOf<
12
+ Extract<keyof Item, 'activeConfig' | 'access' | 'interfaces'>
13
+ >().toEqualTypeOf<never>()
14
+ })
15
+
16
+ test('useInstallations — include shapes each item', () => {
17
+ const result = useInstallations({organizationId: 'org_1', include: ['access']})
18
+ expectTypeOf(result.data.data).toEqualTypeOf<Installation<'access'>[]>()
19
+ type Item = (typeof result.data.data)[number]
20
+ expectTypeOf<Item['access']>().toEqualTypeOf<InstallationAccess[]>()
21
+ })
22
+
23
+ test('useInstallations — requires an organizationId', () => {
24
+ // @ts-expect-error — organizationId is required
25
+ void useInstallations({})
26
+ })
@@ -0,0 +1,25 @@
1
+ import {
2
+ type InstallationInclude,
3
+ installations,
4
+ type InstallationsOptions,
5
+ type InstallationsResponse,
6
+ } from '@sanity/sdk'
7
+
8
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
9
+
10
+ /**
11
+ * Returns the installations matching the given options.
12
+ *
13
+ * The hook suspends until the first fetch succeeds, so `data` is always present.
14
+ * The `include` tokens you pass shape `data.data`: each requested token adds its
15
+ * field, and omitted ones are absent from the type.
16
+ *
17
+ * @public
18
+ * @param options - Filter and include options for the installations list.
19
+ * @returns The result envelope `{data, isFetching, error, refetch}`.
20
+ */
21
+ export const useInstallations = createFetcherHook(installations) as <
22
+ Include extends InstallationInclude = never,
23
+ >(
24
+ options: InstallationsOptions<Include>,
25
+ ) => FetcherHookResult<InstallationsResponse<Include>>
@@ -0,0 +1,60 @@
1
+ import {type Organization, type OrganizationMember} from '@sanity/sdk'
2
+ import {expectTypeOf, test} from 'vitest'
3
+
4
+ import {useOrganization} from './useOrganization'
5
+
6
+ test('useOrganization — no flags: members and features both omitted', () => {
7
+ expectTypeOf(useOrganization({organizationId: 'org_1'}).data).toEqualTypeOf<
8
+ Organization<false, false>
9
+ >()
10
+ })
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
+
19
+ test('useOrganization — includeMembers: true adds members to the type', () => {
20
+ expectTypeOf(useOrganization({organizationId: 'org_1', includeMembers: true}).data).toEqualTypeOf<
21
+ Organization<true, false>
22
+ >()
23
+ type Result = ReturnType<typeof useOrganization<true, false>>
24
+ expectTypeOf<Result['data']['members']>().toEqualTypeOf<OrganizationMember[]>()
25
+ })
26
+
27
+ test('useOrganization — includeFeatures: true adds features to the type', () => {
28
+ expectTypeOf(
29
+ useOrganization({organizationId: 'org_1', includeFeatures: true}).data,
30
+ ).toEqualTypeOf<Organization<false, true>>()
31
+ })
32
+
33
+ test('useOrganization — both flags true → both arrays present', () => {
34
+ expectTypeOf(
35
+ useOrganization({organizationId: 'org_1', includeMembers: true, includeFeatures: true}).data,
36
+ ).toEqualTypeOf<Organization<true, true>>()
37
+ })
38
+
39
+ test('useOrganization — both flags false → bare base shape', () => {
40
+ expectTypeOf(
41
+ useOrganization({organizationId: 'org_1', includeMembers: false, includeFeatures: false}).data,
42
+ ).toEqualTypeOf<Organization<false, false>>()
43
+ type Result = ReturnType<typeof useOrganization<false, false>>
44
+ expectTypeOf<Result['data']['id']>().toEqualTypeOf<string>()
45
+ })
46
+
47
+ test('useOrganization — rejects non-boolean flag values', () => {
48
+ // @ts-expect-error — includeMembers must be a boolean
49
+ void useOrganization({organizationId: 'org_1', includeMembers: 'yes'})
50
+ })
51
+
52
+ test('useOrganization — non-literal boolean flag makes members optional', () => {
53
+ const includeMembers = false as boolean
54
+ expectTypeOf(useOrganization({organizationId: 'org_1', includeMembers}).data).toEqualTypeOf<
55
+ Organization<boolean, false>
56
+ >()
57
+ type Result = ReturnType<typeof useOrganization<boolean, false>>
58
+ expectTypeOf<Result['data']['members']>().toEqualTypeOf<OrganizationMember[] | undefined>()
59
+ expectTypeOf<Pick<Result['data'], 'members'>>().toEqualTypeOf<{members?: OrganizationMember[]}>()
60
+ })
@@ -0,0 +1,63 @@
1
+ import {type Organization, organization, type StateSource} from '@sanity/sdk'
2
+ import {type FetcherSnapshot} from '@sanity/sdk/_internal'
3
+ import {type Observable} from 'rxjs'
4
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
5
+
6
+ import {renderHook} from '../../../test/test-utils'
7
+ import {useOrganization} from './useOrganization'
8
+
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)})
36
+
37
+ describe('useOrganization', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks()
40
+ vi.mocked(organization.getState).mockReturnValue(
41
+ stateSource({id: 'org_1'} as unknown as Organization),
42
+ )
43
+ })
44
+
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
+ })
49
+
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
+ })
56
+
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()
62
+ })
63
+ })
@@ -0,0 +1,34 @@
1
+ import {type Organization, organization, type OrganizationOptions} from '@sanity/sdk'
2
+
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
4
+
5
+ /**
6
+ * Returns metadata for a given organisation.
7
+ *
8
+ * @category Organizations
9
+ * @param options - Configuration options
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`.
13
+ * @example
14
+ * ```tsx
15
+ * function OrganizationName({organizationId}: {organizationId: string}) {
16
+ * const {data: organization} = useOrganization({organizationId})
17
+ *
18
+ * return <h1>{organization.name}</h1>
19
+ * }
20
+ * ```
21
+ * @example
22
+ * ```tsx
23
+ * const {data: organizationWithMembers} = useOrganization({organizationId, includeMembers: true})
24
+ * const {data: organizationWithFeatures} = useOrganization({organizationId, includeFeatures: true})
25
+ * ```
26
+ * @public
27
+ * @function
28
+ */
29
+ export const useOrganization = createFetcherHook(organization) as <
30
+ IncludeMembers extends boolean = false,
31
+ IncludeFeatures extends boolean = false,
32
+ >(
33
+ options: OrganizationOptions<IncludeMembers, IncludeFeatures>,
34
+ ) => FetcherHookResult<Organization<IncludeMembers, IncludeFeatures>>
@@ -0,0 +1,68 @@
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().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>>>()
15
+ })
16
+
17
+ test('useOrganizations — includeMembers: true adds members to the type', () => {
18
+ expectTypeOf(useOrganizations({includeMembers: true}).data).toEqualTypeOf<
19
+ Organizations<true, false>
20
+ >()
21
+ type Result = ReturnType<typeof useOrganizations<true, false>>
22
+ expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<OrganizationMember[]>()
23
+ })
24
+
25
+ test('useOrganizations — includeFeatures: true adds features to the type', () => {
26
+ expectTypeOf(useOrganizations({includeFeatures: true}).data).toEqualTypeOf<
27
+ Organizations<false, true>
28
+ >()
29
+ })
30
+
31
+ test('useOrganizations — both flags true → both arrays present', () => {
32
+ expectTypeOf(useOrganizations({includeMembers: true, includeFeatures: true}).data).toEqualTypeOf<
33
+ Organizations<true, true>
34
+ >()
35
+ })
36
+
37
+ test('useOrganizations — both flags false → bare base shape', () => {
38
+ expectTypeOf(
39
+ useOrganizations({includeMembers: false, includeFeatures: false}).data,
40
+ ).toEqualTypeOf<Organizations<false, false>>()
41
+ type Result = ReturnType<typeof useOrganizations<false, false>>
42
+ expectTypeOf<Result['data'][number]['id']>().toEqualTypeOf<string>()
43
+ })
44
+
45
+ test('useOrganizations — rejects non-boolean flag values', () => {
46
+ // @ts-expect-error — includeMembers must be a boolean
47
+ void useOrganizations({includeMembers: 'yes'})
48
+ })
49
+
50
+ test('useOrganizations — includeImplicitMemberships does not change the data shape', () => {
51
+ expectTypeOf(useOrganizations({includeImplicitMemberships: true}).data).toEqualTypeOf<
52
+ Organizations<false, false>
53
+ >()
54
+ })
55
+
56
+ test('useOrganizations — non-literal boolean flag makes members optional', () => {
57
+ const includeMembers = false as boolean
58
+ expectTypeOf(useOrganizations({includeMembers}).data).toEqualTypeOf<
59
+ Organizations<boolean, false>
60
+ >()
61
+ type Result = ReturnType<typeof useOrganizations<boolean, false>>
62
+ expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<
63
+ OrganizationMember[] | undefined
64
+ >()
65
+ expectTypeOf<Pick<Result['data'][number], 'members'>>().toEqualTypeOf<{
66
+ members?: OrganizationMember[]
67
+ }>()
68
+ })
@@ -0,0 +1,63 @@
1
+ import {type Organizations, organizations, type StateSource} from '@sanity/sdk'
2
+ import {type FetcherSnapshot} from '@sanity/sdk/_internal'
3
+ import {type Observable} from 'rxjs'
4
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
5
+
6
+ import {renderHook} from '../../../test/test-utils'
7
+ import {useOrganizations} from './useOrganizations'
8
+
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)})
36
+
37
+ describe('useOrganizations', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks()
40
+ vi.mocked(organizations.getState).mockReturnValue(
41
+ stateSource([{id: 'org_1'}] as unknown as Organizations),
42
+ )
43
+ })
44
+
45
+ it('reads the organizations fetcher with the passed options', () => {
46
+ renderHook(() => useOrganizations({includeMembers: true}))
47
+ expect(organizations.getState).toHaveBeenCalledWith(sanityInstance, {includeMembers: true})
48
+ })
49
+
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
+ })
56
+
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()
62
+ })
63
+ })
@@ -0,0 +1,39 @@
1
+ import {type Organizations, organizations, type OrganizationsOptions} from '@sanity/sdk'
2
+
3
+ import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
4
+
5
+ /**
6
+ * Returns metadata for each organisation the current user has access to.
7
+ *
8
+ * @category Organizations
9
+ * @param options - Configuration options
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`.
13
+ * @example
14
+ * ```tsx
15
+ * const {data: organizations} = useOrganizations()
16
+ *
17
+ * return (
18
+ * <select>
19
+ * {organizations.map((organization) => (
20
+ * <option key={organization.id}>{organization.name}</option>
21
+ * ))}
22
+ * </select>
23
+ * )
24
+ * ```
25
+ * @example
26
+ * ```tsx
27
+ * const {data: organizationsWithMembers} = useOrganizations({includeMembers: true})
28
+ * const {data: organizationsWithFeatures} = useOrganizations({includeFeatures: true})
29
+ * const {data: organizationsIncludingImplicit} = useOrganizations({includeImplicitMemberships: true})
30
+ * ```
31
+ * @public
32
+ * @function
33
+ */
34
+ export const useOrganizations = createFetcherHook(organizations) as <
35
+ IncludeMembers extends boolean = false,
36
+ IncludeFeatures extends boolean = false,
37
+ >(
38
+ options?: OrganizationsOptions<IncludeMembers, IncludeFeatures>,
39
+ ) => FetcherHookResult<Organizations<IncludeMembers, IncludeFeatures>>
@@ -1,14 +1,20 @@
1
- import {type DatasetResource} from '@sanity/sdk'
1
+ import {act, renderHook} from '@testing-library/react'
2
2
  import {evaluateSync, parse, toJS} from 'groq-js'
3
3
  import {describe, vi} from 'vitest'
4
4
 
5
- import {act, renderHook} from '../../../test/test-utils'
5
+ import {ResourceProvider} from '../../context/ResourceProvider'
6
6
  import {useQuery} from '../query/useQuery'
7
7
  import {usePaginatedDocuments} from './usePaginatedDocuments'
8
8
 
9
9
  vi.mock('../query/useQuery')
10
10
 
11
11
  describe('usePaginatedDocuments', () => {
12
+ const wrapper = ({children}: {children: React.ReactNode}) => (
13
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
14
+ {children}
15
+ </ResourceProvider>
16
+ )
17
+
12
18
  beforeEach(() => {
13
19
  const dataset = [
14
20
  {
@@ -77,14 +83,16 @@ describe('usePaginatedDocuments', () => {
77
83
 
78
84
  it('should respect custom page size', () => {
79
85
  const customPageSize = 2
80
- const {result} = renderHook(() => usePaginatedDocuments({pageSize: customPageSize}))
86
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize: customPageSize}), {wrapper})
81
87
 
82
88
  expect(result.current.pageSize).toBe(customPageSize)
83
89
  expect(result.current.data.length).toBeLessThanOrEqual(customPageSize)
84
90
  })
85
91
 
86
92
  it('should filter by document type', () => {
87
- const {result} = renderHook(() => usePaginatedDocuments({filter: '_type == "movie"'}))
93
+ const {result} = renderHook(() => usePaginatedDocuments({filter: '_type == "movie"'}), {
94
+ wrapper,
95
+ })
88
96
 
89
97
  expect(result.current.data.every((doc) => doc.documentType === 'movie')).toBe(true)
90
98
  expect(result.current.count).toBe(5) // 5 movies in the dataset
@@ -92,18 +100,20 @@ describe('usePaginatedDocuments', () => {
92
100
 
93
101
  // groq-js doesn't support search filters yet
94
102
  it.skip('should apply search filter', () => {
95
- const {result} = renderHook(() => usePaginatedDocuments({search: 'inter'}))
103
+ const {result} = renderHook(() => usePaginatedDocuments({search: 'inter'}), {wrapper})
96
104
 
97
105
  // Should match "Interstellar"
98
106
  expect(result.current.data.some((doc) => doc.documentId === 'movie3')).toBe(true)
99
107
  })
100
108
 
101
109
  it('should apply ordering', () => {
102
- const {result} = renderHook(() =>
103
- usePaginatedDocuments({
104
- filter: '_type == "movie"',
105
- orderings: [{field: 'releaseYear', direction: 'desc'}],
106
- }),
110
+ const {result} = renderHook(
111
+ () =>
112
+ usePaginatedDocuments({
113
+ filter: '_type == "movie"',
114
+ orderings: [{field: 'releaseYear', direction: 'desc'}],
115
+ }),
116
+ {wrapper},
107
117
  )
108
118
 
109
119
  // First item should be the most recent movie (Interstellar, 2014)
@@ -112,7 +122,7 @@ describe('usePaginatedDocuments', () => {
112
122
 
113
123
  it('should calculate pagination values correctly', () => {
114
124
  const pageSize = 2
115
- const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
125
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
116
126
 
117
127
  expect(result.current.currentPage).toBe(1)
118
128
  expect(result.current.totalPages).toBe(3) // 6 items with page size 2
@@ -123,7 +133,7 @@ describe('usePaginatedDocuments', () => {
123
133
 
124
134
  it('should navigate to next page', () => {
125
135
  const pageSize = 2
126
- const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
136
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
127
137
 
128
138
  expect(result.current.currentPage).toBe(1)
129
139
  expect(result.current.data.length).toBe(pageSize)
@@ -139,7 +149,7 @@ describe('usePaginatedDocuments', () => {
139
149
 
140
150
  it('should navigate to previous page', () => {
141
151
  const pageSize = 2
142
- const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
152
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
143
153
 
144
154
  // Go to page 2 first
145
155
  act(() => {
@@ -159,7 +169,7 @@ describe('usePaginatedDocuments', () => {
159
169
 
160
170
  it('should navigate to first page', () => {
161
171
  const pageSize = 2
162
- const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
172
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
163
173
 
164
174
  // Go to last page first
165
175
  act(() => {
@@ -179,7 +189,7 @@ describe('usePaginatedDocuments', () => {
179
189
 
180
190
  it('should navigate to last page', () => {
181
191
  const pageSize = 2
182
- const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
192
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
183
193
 
184
194
  act(() => {
185
195
  result.current.lastPage()
@@ -191,7 +201,7 @@ describe('usePaginatedDocuments', () => {
191
201
 
192
202
  it('should navigate to specific page', () => {
193
203
  const pageSize = 2
194
- const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
204
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
195
205
 
196
206
  act(() => {
197
207
  result.current.goToPage(2) // Go to page 2
@@ -216,7 +226,7 @@ describe('usePaginatedDocuments', () => {
216
226
 
217
227
  it('should set page availability flags correctly', () => {
218
228
  const pageSize = 2
219
- const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
229
+ const {result} = renderHook(() => usePaginatedDocuments({pageSize}), {wrapper})
220
230
  // On first page
221
231
  expect(result.current.hasFirstPage).toBe(false)
222
232
  expect(result.current.hasPreviousPage).toBe(false)
@@ -244,6 +254,7 @@ describe('usePaginatedDocuments', () => {
244
254
  it('should reset current page when filter changes', () => {
245
255
  const {result, rerender} = renderHook((props) => usePaginatedDocuments(props), {
246
256
  initialProps: {pageSize: 2, filter: ''},
257
+ wrapper,
247
258
  })
248
259
  // Initially, current page should be 1
249
260
  expect(result.current.currentPage).toBe(1)
@@ -259,19 +270,15 @@ describe('usePaginatedDocuments', () => {
259
270
  })
260
271
 
261
272
  it('should add projectId and dataset to document handles', () => {
262
- const {result} = renderHook(() => usePaginatedDocuments({}))
273
+ const {result} = renderHook(() => usePaginatedDocuments({}), {wrapper})
263
274
 
264
275
  // Check that the first document handle has the projectId and dataset
265
- expect((result.current.data[0].resource as DatasetResource).projectId).toBe('test')
266
- expect((result.current.data[0].resource as DatasetResource).dataset).toBe('test')
276
+ expect(result.current.data[0].projectId).toBe('p')
277
+ expect(result.current.data[0].dataset).toBe('d')
267
278
 
268
279
  // Verify all document handles have these properties
269
- expect(
270
- result.current.data.every(
271
- (doc) =>
272
- (doc.resource as DatasetResource).projectId === 'test' &&
273
- (doc.resource as DatasetResource).dataset === 'test',
274
- ),
275
- ).toBe(true)
280
+ expect(result.current.data.every((doc) => doc.projectId === 'p' && doc.dataset === 'd')).toBe(
281
+ true,
282
+ )
276
283
  })
277
284
  })