@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.
- package/README.md +103 -155
- package/dist/index.d.ts +1564 -682
- package/dist/index.js +1118 -612
- package/dist/index.js.map +1 -1
- package/package.json +51 -54
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +26 -17
- package/src/components/SDKProvider.test.tsx +173 -32
- package/src/components/SDKProvider.tsx +87 -28
- package/src/components/SanityApp.test.tsx +68 -12
- package/src/components/SanityApp.tsx +94 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +26 -8
- package/src/components/auth/AuthBoundary.tsx +28 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +103 -59
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/CorsErrorComponent.tsx +2 -2
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +29 -27
- package/src/constants.ts +5 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
- package/src/context/DefaultResourceContext.ts +3 -3
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/PerspectiveContext.ts +3 -3
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +72 -90
- package/src/context/ResourceProvider.tsx +59 -93
- package/src/context/SDKStudioContext.test.tsx +28 -33
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/renderSanityApp.test.tsx +151 -49
- package/src/context/renderSanityApp.tsx +12 -8
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +51 -71
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useCreateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useCreateUserApplication.ts +11 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
- package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplication.ts +14 -0
- package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplications.ts +14 -0
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/client/useClient.test.tsx +1 -4
- package/src/hooks/client/useClient.ts +1 -0
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +72 -3
- package/src/hooks/context/useSanityInstance.ts +30 -8
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +10 -16
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +1 -2
- package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
- package/src/hooks/dashboard/useWindowTitle.ts +114 -0
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +33 -13
- package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
- package/src/hooks/document/useApplyDocumentActions.ts +66 -48
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.test.tsx +37 -8
- package/src/hooks/document/useDocument.ts +131 -66
- package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
- package/src/hooks/document/useDocumentEvent.ts +16 -10
- package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
- package/src/hooks/document/useDocumentPermissions.ts +71 -39
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
- package/src/hooks/document/useEditDocument.test.tsx +21 -14
- package/src/hooks/document/useEditDocument.ts +163 -17
- package/src/hooks/documents/useDocuments.test.tsx +64 -44
- package/src/hooks/documents/useDocuments.ts +36 -20
- package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
- package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +92 -50
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
- package/src/hooks/organizations/useOrganization.test.ts +63 -0
- package/src/hooks/organizations/useOrganization.ts +34 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
- package/src/hooks/organizations/useOrganizations.test.ts +63 -0
- package/src/hooks/organizations/useOrganizations.ts +39 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +40 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +9 -20
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +10 -8
- package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
- package/src/hooks/projection/useDocumentProjection.ts +81 -2
- package/src/hooks/projects/useProject.test-d.ts +56 -0
- package/src/hooks/projects/useProject.test.tsx +120 -0
- package/src/hooks/projects/useProject.ts +42 -43
- package/src/hooks/projects/useProjects.test-d.ts +60 -0
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +19 -26
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +91 -9
- package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
- package/src/hooks/releases/useActiveReleases.ts +28 -24
- package/src/hooks/releases/useAllReleases.test.tsx +93 -0
- package/src/hooks/releases/useAllReleases.ts +62 -0
- package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
- package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
- package/src/hooks/releases/usePerspective.test.tsx +18 -10
- package/src/hooks/releases/usePerspective.ts +13 -17
- package/src/hooks/users/useUser.test.tsx +3 -9
- package/src/hooks/users/useUser.ts +2 -0
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +37 -3
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/context/useDefaultResource.test.tsx +0 -25
- package/src/hooks/context/useDefaultResource.ts +0 -30
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
- package/src/hooks/datasets/useDatasets.test.ts +0 -66
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
- 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 {
|
|
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 {
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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(
|
|
266
|
-
expect(
|
|
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
|
-
|
|
271
|
-
|
|
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
|
})
|