@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,120 @@
|
|
|
1
|
+
import {createSanityInstance, type Project, project, type StateSource} from '@sanity/sdk'
|
|
2
|
+
import {type FetcherSnapshot} from '@sanity/sdk/_internal'
|
|
3
|
+
import {type ReactNode} from 'react'
|
|
4
|
+
import {type Observable} from 'rxjs'
|
|
5
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {renderHook} from '../../../test/test-utils'
|
|
8
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
9
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
10
|
+
import {useProject} from './useProject'
|
|
11
|
+
|
|
12
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
13
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
14
|
+
return {...original, project: {getState: vi.fn(), resolveState: vi.fn()}}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const stateSource = (current: Project | undefined): StateSource<FetcherSnapshot<Project>> => {
|
|
18
|
+
// Cache the snapshot: useSyncExternalStore requires a referentially stable current value.
|
|
19
|
+
const snapshot = current
|
|
20
|
+
? {status: 'success', data: current, error: undefined, isFetching: false, dataUpdatedAt: 1}
|
|
21
|
+
: {
|
|
22
|
+
status: 'pending',
|
|
23
|
+
data: undefined,
|
|
24
|
+
error: undefined,
|
|
25
|
+
isFetching: true,
|
|
26
|
+
dataUpdatedAt: undefined,
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
getCurrent: vi.fn(() => snapshot),
|
|
30
|
+
subscribe: vi.fn(() => () => {}),
|
|
31
|
+
get observable(): Observable<unknown> {
|
|
32
|
+
throw new Error('Not implemented')
|
|
33
|
+
},
|
|
34
|
+
} as unknown as StateSource<FetcherSnapshot<Project>>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sanityInstance = expect.objectContaining({config: expect.any(Object)})
|
|
38
|
+
|
|
39
|
+
describe('useProject', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks()
|
|
42
|
+
vi.mocked(project.getState).mockReturnValue(stateSource({id: 'p'} as unknown as Project))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('resolves the projectId from the instance config resource', () => {
|
|
46
|
+
// test-utils wraps with ResourceProvider projectId="test" dataset="test".
|
|
47
|
+
renderHook(() => useProject())
|
|
48
|
+
expect(project.getState).toHaveBeenCalledWith(
|
|
49
|
+
sanityInstance,
|
|
50
|
+
expect.objectContaining({projectId: 'test'}),
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('lets an explicit projectId override the ambient resource', () => {
|
|
55
|
+
renderHook(() => useProject({projectId: 'explicit-project'}))
|
|
56
|
+
expect(project.getState).toHaveBeenCalledWith(
|
|
57
|
+
sanityInstance,
|
|
58
|
+
expect.objectContaining({projectId: 'explicit-project'}),
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('resolves the projectId from an explicit resource when the config has none', () => {
|
|
63
|
+
renderHook(() => useProject(), {
|
|
64
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
65
|
+
<ResourceProvider
|
|
66
|
+
resource={{projectId: 'resource-project', dataset: 'production'}}
|
|
67
|
+
fallback={null}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</ResourceProvider>
|
|
71
|
+
),
|
|
72
|
+
})
|
|
73
|
+
expect(project.getState).toHaveBeenCalledWith(
|
|
74
|
+
sanityInstance,
|
|
75
|
+
expect.objectContaining({projectId: 'resource-project'}),
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('resolves a dataset-less projectId config for project-scoped use', () => {
|
|
80
|
+
renderHook(() => useProject(), {
|
|
81
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
82
|
+
<ResourceProvider projectId="config-project" fallback={null}>
|
|
83
|
+
{children}
|
|
84
|
+
</ResourceProvider>
|
|
85
|
+
),
|
|
86
|
+
})
|
|
87
|
+
// A dataset-less config can't form a DatasetResource; the projectId is carried
|
|
88
|
+
// via ProjectContext and injected so project-scoped reads still resolve it.
|
|
89
|
+
expect(project.getState).toHaveBeenCalledWith(
|
|
90
|
+
sanityInstance,
|
|
91
|
+
expect.objectContaining({projectId: 'config-project'}),
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('resolves a bare projectId from a ResourceProvider when the parent instance has no config', () => {
|
|
96
|
+
// An instance with no project/dataset config and no ambient
|
|
97
|
+
// resource, then a projectId-only ResourceProvider.
|
|
98
|
+
const emptyInstance = createSanityInstance({})
|
|
99
|
+
renderHook(() => useProject(), {
|
|
100
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
101
|
+
<SanityInstanceContext.Provider value={emptyInstance}>
|
|
102
|
+
<ResourceProvider projectId="bare-project" fallback={null}>
|
|
103
|
+
{children}
|
|
104
|
+
</ResourceProvider>
|
|
105
|
+
</SanityInstanceContext.Provider>
|
|
106
|
+
),
|
|
107
|
+
})
|
|
108
|
+
expect(project.getState).toHaveBeenCalledWith(
|
|
109
|
+
emptyInstance,
|
|
110
|
+
expect.objectContaining({projectId: 'bare-project'}),
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('suspends via the project fetcher until project data is available', () => {
|
|
115
|
+
vi.mocked(project.getState).mockReturnValue(stateSource(undefined))
|
|
116
|
+
vi.mocked(project.resolveState).mockReturnValue(new Promise(() => {}))
|
|
117
|
+
renderHook(() => useProject())
|
|
118
|
+
expect(project.resolveState).toHaveBeenCalled()
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -1,50 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getProjectState,
|
|
3
|
-
type ProjectHandle,
|
|
4
|
-
resolveProject,
|
|
5
|
-
type SanityInstance,
|
|
6
|
-
type SanityProject,
|
|
7
|
-
type StateSource,
|
|
8
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {type Project, project, type ProjectOptions} from '@sanity/sdk'
|
|
9
2
|
|
|
10
|
-
import {
|
|
3
|
+
import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
|
|
4
|
+
import {useResolvedProjectId} from '../helpers/useResolvedProjectId'
|
|
11
5
|
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
* Returns metadata for a given project
|
|
16
|
-
*
|
|
17
|
-
* @category Projects
|
|
18
|
-
* @param projectHandle - An optional project handle identifying which project to retrieve metadata for
|
|
19
|
-
* @returns The metadata for the project
|
|
20
|
-
* @example
|
|
21
|
-
* ```tsx
|
|
22
|
-
* function ProjectMetadata({ projectId }: { projectId: string }) {
|
|
23
|
-
* const project = useProject({ projectId })
|
|
24
|
-
*
|
|
25
|
-
* return (
|
|
26
|
-
* <figure style={{ backgroundColor: project.metadata.color || 'lavender'}}>
|
|
27
|
-
* <h1>{project.displayName}</h1>
|
|
28
|
-
* </figure>
|
|
29
|
-
* )
|
|
30
|
-
* }
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
(projectHandle?: ProjectHandle): SanityProject
|
|
34
|
-
}
|
|
6
|
+
const useProjectBase = createFetcherHook(project)
|
|
35
7
|
|
|
36
8
|
/**
|
|
9
|
+
* Returns metadata for a given project.
|
|
10
|
+
*
|
|
11
|
+
* @category Projects
|
|
12
|
+
* @param options - Configuration options
|
|
13
|
+
* @returns A {@link FetcherHookResult} whose `data` is the metadata for the
|
|
14
|
+
* project. `members` is included only when `includeMembers: true`; `features`
|
|
15
|
+
* is included unless `includeFeatures: false`.
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* function ProjectMetadata({projectId}: {projectId: string}) {
|
|
19
|
+
* const {data: project} = useProject({projectId})
|
|
20
|
+
*
|
|
21
|
+
* return (
|
|
22
|
+
* <figure style={{backgroundColor: project.metadata.color || 'lavender'}}>
|
|
23
|
+
* <h1>{project.displayName}</h1>
|
|
24
|
+
* </figure>
|
|
25
|
+
* )
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* const {data: projectWithMembersAndFeatures} = useProject({projectId})
|
|
31
|
+
* const {data: projectWithMembers} = useProject({projectId, includeMembers: true})
|
|
32
|
+
* const {data: projectWithoutMembers} = useProject({projectId, includeMembers: false})
|
|
33
|
+
* const {data: projectWithoutFeatures} = useProject({projectId, includeFeatures: false})
|
|
34
|
+
* ```
|
|
35
|
+
* @remarks
|
|
36
|
+
* The `projectId` is resolved in order from:
|
|
37
|
+
* 1. an explicit `projectId` option
|
|
38
|
+
* 2. A legacy ProjectContext (e.g. a `<ResourceProvider projectId="…">` with no dataset), then
|
|
39
|
+
* 3. The active resource (`ResourceProvider`/`SDKProvider`)
|
|
40
|
+
* 4. `instance.config`.
|
|
37
41
|
* @public
|
|
38
42
|
* @function
|
|
39
43
|
*/
|
|
40
|
-
export const useProject
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
shouldSuspend: (instance: SanityInstance, projectHandle?: ProjectHandle) =>
|
|
47
|
-
getProjectState(instance, projectHandle as ProjectHandle).getCurrent() === undefined,
|
|
48
|
-
suspender: (instance: SanityInstance, projectHandle?: ProjectHandle) =>
|
|
49
|
-
resolveProject(instance, projectHandle as ProjectHandle),
|
|
50
|
-
}) as UseProject
|
|
44
|
+
export const useProject = ((options?: ProjectOptions<boolean, boolean>) => {
|
|
45
|
+
const projectId = useResolvedProjectId(options)
|
|
46
|
+
return useProjectBase(projectId ? {...options, projectId} : options)
|
|
47
|
+
}) as <IncludeMembers extends boolean = true, IncludeFeatures extends boolean = true>(
|
|
48
|
+
options?: ProjectOptions<IncludeMembers, IncludeFeatures>,
|
|
49
|
+
) => FetcherHookResult<Project<IncludeMembers, IncludeFeatures>>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {type Project, type ProjectMember} from '@sanity/sdk'
|
|
2
|
+
import {expectTypeOf, test} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useProjects} from './useProjects'
|
|
5
|
+
|
|
6
|
+
test('useProjects — no args: features included, members omitted', () => {
|
|
7
|
+
expectTypeOf(useProjects().data).toEqualTypeOf<Project<false, true>[]>()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
test('useProjects — returns the FetcherHookResult envelope', () => {
|
|
11
|
+
const result = useProjects()
|
|
12
|
+
expectTypeOf(result.isFetching).toEqualTypeOf<boolean>()
|
|
13
|
+
expectTypeOf(result.error).toEqualTypeOf<unknown>()
|
|
14
|
+
expectTypeOf(result.refetch).toEqualTypeOf<() => Promise<Project<false, true>[]>>()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('useProjects — includeMembers: true adds members to the type', () => {
|
|
18
|
+
expectTypeOf(useProjects({includeMembers: true}).data).toEqualTypeOf<Project<true, true>[]>()
|
|
19
|
+
type Result = ReturnType<typeof useProjects<true, true>>
|
|
20
|
+
expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<ProjectMember[]>()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('useProjects — includeFeatures: false drops features from the type', () => {
|
|
24
|
+
expectTypeOf(useProjects({includeFeatures: false}).data).toEqualTypeOf<Project<false, false>[]>()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('useProjects — both flags true → both arrays present', () => {
|
|
28
|
+
expectTypeOf(useProjects({includeMembers: true, includeFeatures: true}).data).toEqualTypeOf<
|
|
29
|
+
Project<true, true>[]
|
|
30
|
+
>()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('useProjects — both flags false → bare base shape', () => {
|
|
34
|
+
expectTypeOf(useProjects({includeMembers: false, includeFeatures: false}).data).toEqualTypeOf<
|
|
35
|
+
Project<false, false>[]
|
|
36
|
+
>()
|
|
37
|
+
type Result = ReturnType<typeof useProjects<false, false>>
|
|
38
|
+
expectTypeOf<Result['data'][number]['id']>().toEqualTypeOf<string>()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('useProjects — rejects non-boolean flag values', () => {
|
|
42
|
+
// @ts-expect-error — includeMembers must be a boolean
|
|
43
|
+
void useProjects({includeMembers: 'yes'})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('useProjects — organizationId alone does not change the data shape', () => {
|
|
47
|
+
expectTypeOf(useProjects({organizationId: 'org_123'}).data).toEqualTypeOf<
|
|
48
|
+
Project<false, true>[]
|
|
49
|
+
>()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('useProjects — non-literal boolean flag makes members optional', () => {
|
|
53
|
+
const includeMembers = false as boolean
|
|
54
|
+
expectTypeOf(useProjects({includeMembers}).data).toEqualTypeOf<Project<boolean, true>[]>()
|
|
55
|
+
type Result = ReturnType<typeof useProjects<boolean, true>>
|
|
56
|
+
expectTypeOf<Result['data'][number]['members']>().toEqualTypeOf<ProjectMember[] | undefined>()
|
|
57
|
+
expectTypeOf<Pick<Result['data'][number], 'members'>>().toEqualTypeOf<{
|
|
58
|
+
members?: ProjectMember[]
|
|
59
|
+
}>()
|
|
60
|
+
})
|
|
@@ -1,112 +1,59 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {type Project, projects, type StateSource} from '@sanity/sdk'
|
|
2
|
+
import {type FetcherSnapshot} from '@sanity/sdk/_internal'
|
|
3
|
+
import {type Observable} from 'rxjs'
|
|
2
4
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
5
|
|
|
4
|
-
import {
|
|
6
|
+
import {renderHook} from '../../../test/test-utils'
|
|
7
|
+
import {useProjects} from './useProjects'
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
10
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
11
|
+
return {...original, projects: {getState: vi.fn(), resolveState: vi.fn(), refetch: vi.fn()}}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const stateSource = (current: Project[] | undefined): StateSource<FetcherSnapshot<Project[]>> => {
|
|
15
|
+
const snapshot = current
|
|
16
|
+
? {status: 'success', data: current, error: undefined, isFetching: false, dataUpdatedAt: 1}
|
|
17
|
+
: {
|
|
18
|
+
status: 'pending',
|
|
19
|
+
data: undefined,
|
|
20
|
+
error: undefined,
|
|
21
|
+
isFetching: true,
|
|
22
|
+
dataUpdatedAt: undefined,
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
getCurrent: vi.fn(() => snapshot),
|
|
26
|
+
subscribe: vi.fn(() => () => {}),
|
|
27
|
+
get observable(): Observable<unknown> {
|
|
28
|
+
throw new Error('Not implemented')
|
|
29
|
+
},
|
|
30
|
+
} as unknown as StateSource<FetcherSnapshot<Project[]>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const sanityInstance = expect.objectContaining({config: expect.any(Object)})
|
|
16
34
|
|
|
17
35
|
describe('useProjects', () => {
|
|
18
|
-
// Use beforeEach to reset modules and ensure mocks are fresh for each test
|
|
19
36
|
beforeEach(() => {
|
|
20
|
-
vi.
|
|
21
|
-
|
|
22
|
-
vi.mock('@sanity/sdk', () => ({
|
|
23
|
-
getProjectsState: vi.fn(() => ({
|
|
24
|
-
getCurrent: vi.fn(() => undefined),
|
|
25
|
-
})),
|
|
26
|
-
resolveProjects: vi.fn(),
|
|
27
|
-
}))
|
|
28
|
-
vi.mock('../helpers/createStateSourceHook', () => ({
|
|
29
|
-
createStateSourceHook: vi.fn(),
|
|
30
|
-
}))
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('should call createStateSourceHook with correct arguments on import', async () => {
|
|
34
|
-
// Dynamically import the hook *after* mocks are set up and modules reset
|
|
35
|
-
await import('./useProjects')
|
|
36
|
-
|
|
37
|
-
// Check if createStateSourceHook was called during the module evaluation (import)
|
|
38
|
-
expect(createStateSourceHook).toHaveBeenCalled()
|
|
39
|
-
expect(createStateSourceHook).toHaveBeenCalledWith(
|
|
40
|
-
expect.objectContaining({
|
|
41
|
-
getState: expect.any(Function),
|
|
42
|
-
shouldSuspend: expect.any(Function),
|
|
43
|
-
suspender: expect.any(Function), // Actual function reference doesn't matter here as it's mocked
|
|
44
|
-
// Note: getConfig is not used in useProjects
|
|
45
|
-
}),
|
|
46
|
-
)
|
|
37
|
+
vi.clearAllMocks()
|
|
38
|
+
vi.mocked(projects.getState).mockReturnValue(stateSource([{id: 'a'}] as unknown as Project[]))
|
|
47
39
|
})
|
|
48
40
|
|
|
49
|
-
it('
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Get the arguments passed to createStateSourceHook
|
|
54
|
-
const mockCreateStateSourceHook = createStateSourceHook as ReturnType<typeof vi.fn>
|
|
55
|
-
expect(mockCreateStateSourceHook.mock.calls.length).toBeGreaterThan(0)
|
|
56
|
-
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
|
|
57
|
-
const shouldSuspend = createStateSourceHookArgs.shouldSuspend
|
|
58
|
-
|
|
59
|
-
// Mock instance for the test call
|
|
60
|
-
const mockInstance = {} as SanityInstance // Use specific type
|
|
61
|
-
|
|
62
|
-
// Call the shouldSuspend function with both required parameters
|
|
63
|
-
const result = shouldSuspend(mockInstance, undefined) // Pass undefined for options
|
|
64
|
-
|
|
65
|
-
// Assert that getProjectsState was called with the correct arguments
|
|
66
|
-
const mockGetProjectsState = getProjectsState as ReturnType<typeof vi.fn>
|
|
67
|
-
expect(mockGetProjectsState).toHaveBeenCalledWith(mockInstance, undefined)
|
|
68
|
-
|
|
69
|
-
// Assert that getCurrent was called on the result of getProjectsState
|
|
70
|
-
expect(mockGetProjectsState.mock.results.length).toBeGreaterThan(0)
|
|
71
|
-
const getProjectsStateMockResult = mockGetProjectsState.mock.results[0].value
|
|
72
|
-
expect(getProjectsStateMockResult.getCurrent).toHaveBeenCalled()
|
|
73
|
-
|
|
74
|
-
// Assert the result of shouldSuspend based on the mocked getCurrent value
|
|
75
|
-
expect(result).toBe(true) // Since getCurrent is mocked to return undefined
|
|
41
|
+
it('reads the projects fetcher with the passed options', () => {
|
|
42
|
+
renderHook(() => useProjects({organizationId: 'org123'}))
|
|
43
|
+
expect(projects.getState).toHaveBeenCalledWith(sanityInstance, {organizationId: 'org123'})
|
|
76
44
|
})
|
|
77
45
|
|
|
78
|
-
it('
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
expect(
|
|
83
|
-
|
|
84
|
-
const createStateSourceHookArgs = mockCreateStateSourceHook.mock.calls[0][0]
|
|
85
|
-
const getState = createStateSourceHookArgs.getState
|
|
86
|
-
|
|
87
|
-
// Test that getState can handle the new options parameter
|
|
88
|
-
const mockInstance = {} as SanityInstance
|
|
89
|
-
const mockOptions = {organizationId: 'org123', includeMembers: false}
|
|
90
|
-
|
|
91
|
-
// This should not throw
|
|
92
|
-
expect(() => getState(mockInstance, mockOptions)).not.toThrow()
|
|
46
|
+
it('returns the fetcher data in the result envelope', () => {
|
|
47
|
+
const {result} = renderHook(() => useProjects())
|
|
48
|
+
expect(result.current.data).toEqual([{id: 'a'}])
|
|
49
|
+
expect(result.current.isFetching).toBe(false)
|
|
50
|
+
expect(typeof result.current.refetch).toBe('function')
|
|
93
51
|
})
|
|
94
52
|
|
|
95
|
-
it('
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const shouldSuspend = createStateSourceHookArgs.shouldSuspend
|
|
101
|
-
|
|
102
|
-
const mockInstance = {} as SanityInstance
|
|
103
|
-
|
|
104
|
-
// Test with different options
|
|
105
|
-
expect(() => shouldSuspend(mockInstance, undefined)).not.toThrow()
|
|
106
|
-
expect(() => shouldSuspend(mockInstance, {organizationId: 'org123'})).not.toThrow()
|
|
107
|
-
expect(() => shouldSuspend(mockInstance, {includeMembers: false})).not.toThrow()
|
|
108
|
-
expect(() =>
|
|
109
|
-
shouldSuspend(mockInstance, {organizationId: 'org123', includeMembers: false}),
|
|
110
|
-
).not.toThrow()
|
|
53
|
+
it('suspends via the projects fetcher until data is available', () => {
|
|
54
|
+
vi.mocked(projects.getState).mockReturnValue(stateSource(undefined))
|
|
55
|
+
vi.mocked(projects.resolveState).mockReturnValue(new Promise(() => {}))
|
|
56
|
+
renderHook(() => useProjects())
|
|
57
|
+
expect(projects.resolveState).toHaveBeenCalled()
|
|
111
58
|
})
|
|
112
59
|
})
|
|
@@ -1,33 +1,26 @@
|
|
|
1
|
-
import {type
|
|
2
|
-
import {getProjectsState, resolveProjects, type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
1
|
+
import {type Project, projects, type ProjectsOptions} from '@sanity/sdk'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* @public
|
|
8
7
|
* @category Types
|
|
9
8
|
* @interface
|
|
9
|
+
* @deprecated use the Project type directly.
|
|
10
10
|
*/
|
|
11
|
-
export type ProjectWithoutMembers =
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @public
|
|
15
|
-
* @category Types
|
|
16
|
-
*/
|
|
17
|
-
type UseProjects = <TIncludeMembers extends boolean = false>(options?: {
|
|
18
|
-
organizationId?: string
|
|
19
|
-
includeMembers?: TIncludeMembers
|
|
20
|
-
}) => TIncludeMembers extends true ? SanityProject[] : ProjectWithoutMembers[]
|
|
11
|
+
export type ProjectWithoutMembers = Project
|
|
21
12
|
|
|
22
13
|
/**
|
|
23
14
|
* Returns metadata for each project you have access to.
|
|
24
15
|
*
|
|
25
16
|
* @category Projects
|
|
26
17
|
* @param options - Configuration options
|
|
27
|
-
* @returns
|
|
18
|
+
* @returns A {@link FetcherHookResult} whose `data` is an array of project
|
|
19
|
+
* metadata. `members` is included only when `includeMembers: true`; `features`
|
|
20
|
+
* is included unless `includeFeatures: false`.
|
|
28
21
|
* @example
|
|
29
22
|
* ```tsx
|
|
30
|
-
* const projects = useProjects()
|
|
23
|
+
* const {data: projects} = useProjects()
|
|
31
24
|
*
|
|
32
25
|
* return (
|
|
33
26
|
* <select>
|
|
@@ -39,18 +32,18 @@ type UseProjects = <TIncludeMembers extends boolean = false>(options?: {
|
|
|
39
32
|
* ```
|
|
40
33
|
* @example
|
|
41
34
|
* ```tsx
|
|
42
|
-
* const
|
|
43
|
-
* const
|
|
35
|
+
* const {data: projects} = useProjects()
|
|
36
|
+
* const {data: projectsWithFeatures} = useProjects()
|
|
37
|
+
* const {data: projectsWithMembers} = useProjects({includeMembers: true})
|
|
38
|
+
* const {data: projectsWithoutMembers} = useProjects({includeMembers: false})
|
|
39
|
+
* const {data: projectsWithoutFeatures} = useProjects({includeFeatures: false})
|
|
44
40
|
* ```
|
|
45
41
|
* @public
|
|
46
42
|
* @function
|
|
47
43
|
*/
|
|
48
|
-
export const useProjects
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
getProjectsState(instance, options).getCurrent() === undefined,
|
|
55
|
-
suspender: resolveProjects,
|
|
56
|
-
}) as UseProjects
|
|
44
|
+
export const useProjects = createFetcherHook(projects) as <
|
|
45
|
+
IncludeMembers extends boolean = false,
|
|
46
|
+
IncludeFeatures extends boolean = true,
|
|
47
|
+
>(
|
|
48
|
+
options?: ProjectsOptions<IncludeMembers, IncludeFeatures>,
|
|
49
|
+
) => FetcherHookResult<Project<IncludeMembers, IncludeFeatures>[]>
|
|
@@ -33,7 +33,7 @@ describe('useQuery', () => {
|
|
|
33
33
|
} as StateSource<unknown>)
|
|
34
34
|
|
|
35
35
|
function TestComponent() {
|
|
36
|
-
const {data, isPending} = useQuery
|
|
36
|
+
const {data, isPending} = useQuery({query: 'test query'})
|
|
37
37
|
return (
|
|
38
38
|
<div data-testid="output">
|
|
39
39
|
{data} - {isPending ? 'pending' : 'not pending'}
|
|
@@ -42,7 +42,7 @@ describe('useQuery', () => {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
render(
|
|
45
|
-
<ResourceProvider
|
|
45
|
+
<ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
|
|
46
46
|
<TestComponent />
|
|
47
47
|
</ResourceProvider>,
|
|
48
48
|
)
|
|
@@ -82,13 +82,14 @@ describe('useQuery', () => {
|
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
function TestComponent() {
|
|
85
|
-
const {data} = useQuery
|
|
85
|
+
const {data} = useQuery({query: 'test query'})
|
|
86
86
|
return <div data-testid="output">{data}</div>
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
render(
|
|
90
90
|
<ResourceProvider
|
|
91
|
-
|
|
91
|
+
projectId="p"
|
|
92
|
+
dataset="d"
|
|
92
93
|
fallback={<div data-testid="fallback">Loading...</div>}
|
|
93
94
|
>
|
|
94
95
|
<TestComponent />
|
|
@@ -163,7 +164,7 @@ describe('useQuery', () => {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
render(
|
|
166
|
-
<ResourceProvider
|
|
167
|
+
<ResourceProvider projectId="p" dataset="d" fallback={<p>Loading...</p>}>
|
|
167
168
|
<WrapperComponent />
|
|
168
169
|
</ResourceProvider>,
|
|
169
170
|
)
|