@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2
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 +2488 -2019
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4435 -1529
- package/dist/index.js.map +1 -1
- package/package.json +55 -59
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +30 -18
- 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 +93 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +7 -7
- package/src/components/auth/AuthBoundary.tsx +29 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +104 -61
- 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/ComlinkTokenRefresh.tsx +3 -5
- 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 +93 -90
- package/src/context/ResourceProvider.tsx +62 -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 +2 -2
- package/src/hooks/agent/agentActions.ts +52 -72
- package/src/hooks/agent/useAgentResourceContext.ts +1 -1
- 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/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -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/comlink/useFrameConnection.test.tsx +3 -2
- package/src/hooks/comlink/useFrameConnection.ts +1 -1
- package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
- package/src/hooks/comlink/useWindowConnection.ts +2 -3
- package/src/hooks/comments/useCommentActions.test.tsx +150 -0
- package/src/hooks/comments/useCommentActions.ts +109 -0
- package/src/hooks/comments/useCommentList.ts +79 -0
- package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
- package/src/hooks/comments/useCommentThreads.ts +73 -0
- package/src/hooks/comments/useComments.test.tsx +242 -0
- package/src/hooks/comments/useComments.ts +59 -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 +18 -1
- package/src/hooks/context/useSanityInstance.ts +6 -6
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +11 -17
- 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 +2 -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 +31 -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 +226 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
- 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 +35 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +22 -20
- package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
- package/src/hooks/presence/usePresenceForDocument.ts +104 -0
- package/src/hooks/presence/useReportPresence.test.tsx +202 -0
- package/src/hooks/presence/useReportPresence.ts +170 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +12 -14
- 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 +17 -32
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +94 -17
- 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 +4 -8
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +38 -5
- 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,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
|
})
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {type DocumentHandle, isDatasetResource, type QueryOptions} from '@sanity/sdk'
|
|
2
|
+
import {createGroqSearchFilter, pickProperties} from '@sanity/sdk/_internal'
|
|
2
3
|
import {type SortOrderingItem} from '@sanity/types'
|
|
3
|
-
import {
|
|
4
|
-
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
4
|
+
import {useCallback, useMemo, useState} from 'react'
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
useNormalizedResourceOptions,
|
|
8
|
+
type WithResourceNameSupport,
|
|
9
|
+
} from '../helpers/useNormalizedResourceOptions'
|
|
10
|
+
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
8
11
|
import {useQuery} from '../query/useQuery'
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -17,10 +20,9 @@ export interface PaginatedDocumentsOptions<
|
|
|
17
20
|
TDocumentType extends string = string,
|
|
18
21
|
TDataset extends string = string,
|
|
19
22
|
TProjectId extends string = string,
|
|
20
|
-
>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Pick<QueryOptions<TDocumentType, TDataset, TProjectId>, 'params'> {
|
|
23
|
+
> extends WithResourceNameSupport<
|
|
24
|
+
Omit<QueryOptions<TDocumentType, TDataset, TProjectId>, 'query'>
|
|
25
|
+
> {
|
|
24
26
|
documentType?: TDocumentType | TDocumentType[]
|
|
25
27
|
/**
|
|
26
28
|
* GROQ filter expression to apply to the query
|
|
@@ -141,16 +143,18 @@ export interface PaginatedDocumentsResponse<
|
|
|
141
143
|
* @returns An object containing the list of document handles, pagination details, and functions to navigate between pages
|
|
142
144
|
*
|
|
143
145
|
* @remarks
|
|
144
|
-
* - The returned document handles include
|
|
146
|
+
* - The returned document handles include projectId and dataset information from the current Sanity instance
|
|
145
147
|
* - This makes them ready to use with document operations and other document hooks
|
|
146
|
-
* - The hook automatically uses the correct Sanity instance based on the
|
|
148
|
+
* - The hook automatically uses the correct Sanity instance based on the projectId and dataset in the options
|
|
147
149
|
*
|
|
148
150
|
* @example Paginated list of documents with navigation
|
|
149
151
|
* ```tsx
|
|
150
152
|
* import {
|
|
151
153
|
* usePaginatedDocuments,
|
|
154
|
+
* createDatasetHandle,
|
|
155
|
+
* type DatasetHandle,
|
|
152
156
|
* type DocumentHandle,
|
|
153
|
-
* type
|
|
157
|
+
* type SortOrderingItem,
|
|
154
158
|
* useDocumentProjection
|
|
155
159
|
* } from '@sanity/sdk-react'
|
|
156
160
|
* import {Suspense} from 'react'
|
|
@@ -173,10 +177,10 @@ export interface PaginatedDocumentsResponse<
|
|
|
173
177
|
* // Define props for the list component
|
|
174
178
|
* interface PaginatedDocumentListProps {
|
|
175
179
|
* documentType: string
|
|
176
|
-
*
|
|
180
|
+
* dataset?: DatasetHandle
|
|
177
181
|
* }
|
|
178
182
|
*
|
|
179
|
-
* function PaginatedDocumentList({documentType,
|
|
183
|
+
* function PaginatedDocumentList({documentType, dataset}: PaginatedDocumentListProps) {
|
|
180
184
|
* const {
|
|
181
185
|
* data,
|
|
182
186
|
* isPending,
|
|
@@ -187,7 +191,7 @@ export interface PaginatedDocumentsResponse<
|
|
|
187
191
|
* hasNextPage,
|
|
188
192
|
* hasPreviousPage
|
|
189
193
|
* } = usePaginatedDocuments({
|
|
190
|
-
*
|
|
194
|
+
* ...dataset,
|
|
191
195
|
* documentType,
|
|
192
196
|
* pageSize: 10,
|
|
193
197
|
* orderings: [{field: '_createdAt', direction: 'desc'}],
|
|
@@ -219,7 +223,8 @@ export interface PaginatedDocumentsResponse<
|
|
|
219
223
|
* }
|
|
220
224
|
*
|
|
221
225
|
* // Usage:
|
|
222
|
-
* //
|
|
226
|
+
* // const myDatasetHandle = createDatasetHandle({ projectId: 'p1', dataset: 'production' })
|
|
227
|
+
* // <PaginatedDocumentList dataset={myDatasetHandle} documentType="post" />
|
|
223
228
|
* ```
|
|
224
229
|
*/
|
|
225
230
|
export function usePaginatedDocuments<
|
|
@@ -239,14 +244,16 @@ export function usePaginatedDocuments<
|
|
|
239
244
|
TDataset,
|
|
240
245
|
TProjectId
|
|
241
246
|
> {
|
|
247
|
+
useTrackHookUsage('usePaginatedDocuments')
|
|
242
248
|
const options = useNormalizedResourceOptions(rawOptions)
|
|
243
249
|
const [pageIndex, setPageIndex] = useState(0)
|
|
244
250
|
const key = JSON.stringify({filter, search, params, orderings, pageSize, ...options})
|
|
245
|
-
// Reset
|
|
246
|
-
|
|
247
|
-
|
|
251
|
+
// Reset pageIndex to 0 whenever any query parameter changes.
|
|
252
|
+
const [prevKey, setPrevKey] = useState(key)
|
|
253
|
+
if (prevKey !== key) {
|
|
254
|
+
setPrevKey(key)
|
|
248
255
|
setPageIndex(0)
|
|
249
|
-
}
|
|
256
|
+
}
|
|
250
257
|
|
|
251
258
|
const startIndex = pageIndex * pageSize
|
|
252
259
|
const endIndex = (pageIndex + 1) * pageSize
|
|
@@ -301,8 +308,14 @@ export function usePaginatedDocuments<
|
|
|
301
308
|
params: {
|
|
302
309
|
...params,
|
|
303
310
|
__types: documentTypes,
|
|
304
|
-
|
|
305
|
-
|
|
311
|
+
__handle: {
|
|
312
|
+
// keep projectId/dataset for backward compat until v4; resource is added
|
|
313
|
+
// intentionally so that hook consumers can resolve the correct resource
|
|
314
|
+
...(options.resource && isDatasetResource(options.resource)
|
|
315
|
+
? pickProperties(options.resource, ['projectId', 'dataset'])
|
|
316
|
+
: {}),
|
|
317
|
+
...pickProperties(options, ['perspective', 'resource']),
|
|
318
|
+
},
|
|
306
319
|
},
|
|
307
320
|
})
|
|
308
321
|
|
|
@@ -102,21 +102,6 @@ describe('usePresence', () => {
|
|
|
102
102
|
}).toThrow('usePresence() does not support media library resources')
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
it('should throw an error when used with a canvas resource', () => {
|
|
106
|
-
expect(() => {
|
|
107
|
-
renderHook(() => usePresence({resource: {canvasId: 'canvas123'}}), {
|
|
108
|
-
wrapper: ({children}) => (
|
|
109
|
-
<ResourceProvider
|
|
110
|
-
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
111
|
-
fallback={null}
|
|
112
|
-
>
|
|
113
|
-
{children}
|
|
114
|
-
</ResourceProvider>
|
|
115
|
-
),
|
|
116
|
-
})
|
|
117
|
-
}).toThrow('usePresence() does not support canvas resources')
|
|
118
|
-
})
|
|
119
|
-
|
|
120
105
|
it('should work with a dataset resource', () => {
|
|
121
106
|
const mockPresenceSource = {
|
|
122
107
|
getCurrent: vi.fn().mockReturnValue([]),
|
|
@@ -1,38 +1,40 @@
|
|
|
1
|
-
import {getPresence,
|
|
1
|
+
import {getPresence, isMediaLibraryResource, type UserPresence} from '@sanity/sdk'
|
|
2
2
|
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
3
3
|
|
|
4
4
|
import {type ResourceHandle} from '../../config/handles'
|
|
5
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
6
|
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
7
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Every participant in the current project and dataset, or Canvas.
|
|
11
|
+
*
|
|
12
|
+
* Reading presence never announces anything. Call `useReportPresence` to make the
|
|
13
|
+
* current user visible to others, including to the Studio, which shares the same
|
|
14
|
+
* presence room.
|
|
15
|
+
*
|
|
16
|
+
* This returns everyone in the whole resource and leaves the filtering to you.
|
|
17
|
+
* Prefer `usePresenceForDocument` when you care about one document: it scopes and
|
|
18
|
+
* flattens the result for rendering. Note that participants are counted by session,
|
|
19
|
+
* so one person in two tabs appears twice.
|
|
20
|
+
*
|
|
21
|
+
* Presence is scoped to a single project and dataset. It is not a list of everyone
|
|
22
|
+
* signed in to your organization.
|
|
23
|
+
*
|
|
10
24
|
* @public
|
|
11
25
|
*/
|
|
12
26
|
export function usePresence(options: ResourceHandle = {}): {
|
|
13
27
|
locations: UserPresence[]
|
|
14
28
|
} {
|
|
15
29
|
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (normalizedOptions.resource) {
|
|
21
|
-
if (isMediaLibraryResource(normalizedOptions.resource)) {
|
|
22
|
-
throw new Error(
|
|
23
|
-
'usePresence() does not support media library resources. Presence tracking requires a dataset resource. ' +
|
|
24
|
-
'Either remove the resourceName parameter or use a dataset resource instead.',
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (isCanvasResource(normalizedOptions.resource)) {
|
|
29
|
-
throw new Error(
|
|
30
|
-
'usePresence() does not support canvas resources. Presence tracking requires a dataset resource. ' +
|
|
31
|
-
'Either remove the resourceName parameter or use a dataset resource instead.',
|
|
32
|
-
)
|
|
33
|
-
}
|
|
30
|
+
if (normalizedOptions.resource && isMediaLibraryResource(normalizedOptions.resource)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'usePresence() does not support media library resources. Presence tracking requires a canvas or dataset resource.',
|
|
33
|
+
)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const sanityInstance = useSanityInstance()
|
|
37
|
+
trackHookUsage(sanityInstance, 'usePresence')
|
|
36
38
|
const source = useMemo(
|
|
37
39
|
() => getPresence(sanityInstance, normalizedOptions),
|
|
38
40
|
[sanityInstance, normalizedOptions],
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {type DocumentPresence, getDocumentPresence} from '@sanity/sdk'
|
|
2
|
+
import {act, renderHook} from '@testing-library/react'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
6
|
+
import {usePresenceForDocument} from './usePresenceForDocument'
|
|
7
|
+
|
|
8
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import('@sanity/sdk')>()
|
|
10
|
+
return {...actual, getDocumentPresence: vi.fn()}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const entry = (sessionId: string): DocumentPresence =>
|
|
14
|
+
({
|
|
15
|
+
sessionId,
|
|
16
|
+
documentId: 'movie-1',
|
|
17
|
+
path: ['title'],
|
|
18
|
+
lastActiveAt: '2026-07-30T12:00:00Z',
|
|
19
|
+
user: {sanityUserId: 'u1', profile: {id: 'u1'}, memberships: []},
|
|
20
|
+
}) as unknown as DocumentPresence
|
|
21
|
+
|
|
22
|
+
/** A minimal StateSource stand-in whose value can be pushed. */
|
|
23
|
+
function createSource(initial: DocumentPresence[]) {
|
|
24
|
+
let current = initial
|
|
25
|
+
const listeners = new Set<() => void>()
|
|
26
|
+
return {
|
|
27
|
+
source: {
|
|
28
|
+
getCurrent: () => current,
|
|
29
|
+
subscribe: (cb: () => void) => {
|
|
30
|
+
listeners.add(cb)
|
|
31
|
+
return () => listeners.delete(cb)
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
push: (next: DocumentPresence[]) => {
|
|
35
|
+
current = next
|
|
36
|
+
listeners.forEach((cb) => cb())
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
42
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
43
|
+
{children}
|
|
44
|
+
</ResourceProvider>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
describe('usePresenceForDocument', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
// Without this, `mock.calls[0]` is whatever the previous test did.
|
|
50
|
+
vi.clearAllMocks()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('returns presence and updates when the store changes', () => {
|
|
54
|
+
const {source, push} = createSource([entry('s1')])
|
|
55
|
+
vi.mocked(getDocumentPresence).mockReturnValue(source as never)
|
|
56
|
+
|
|
57
|
+
const {result} = renderHook(
|
|
58
|
+
() => usePresenceForDocument({documentId: 'movie-1', documentType: 'movie'}),
|
|
59
|
+
{wrapper},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect(result.current.presence).toHaveLength(1)
|
|
63
|
+
|
|
64
|
+
act(() => push([entry('s1'), entry('s2')]))
|
|
65
|
+
expect(result.current.presence).toHaveLength(2)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('passes the document, path, and excludeVersions through', () => {
|
|
69
|
+
const {source} = createSource([])
|
|
70
|
+
vi.mocked(getDocumentPresence).mockReturnValue(source as never)
|
|
71
|
+
|
|
72
|
+
renderHook(
|
|
73
|
+
() =>
|
|
74
|
+
usePresenceForDocument({
|
|
75
|
+
documentId: 'drafts.movie-1',
|
|
76
|
+
documentType: 'movie',
|
|
77
|
+
path: ['cast', {_key: 'm1'}],
|
|
78
|
+
excludeVersions: true,
|
|
79
|
+
}),
|
|
80
|
+
{wrapper},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Already a draft id, so resolution leaves it alone.
|
|
84
|
+
expect(vi.mocked(getDocumentPresence).mock.calls[0][1]).toMatchObject({
|
|
85
|
+
documentId: 'drafts.movie-1',
|
|
86
|
+
path: ['cast', {_key: 'm1'}],
|
|
87
|
+
excludeVersions: true,
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('reads the same id useReportPresence writes, or reads never match writes', () => {
|
|
92
|
+
const {source} = createSource([])
|
|
93
|
+
vi.mocked(getDocumentPresence).mockReturnValue(source as never)
|
|
94
|
+
|
|
95
|
+
// Forwarded unresolved, exactly as `useReportPresence` forwards it, so core
|
|
96
|
+
// resolves both the same way. If these diverged, field-level presence would
|
|
97
|
+
// silently never match.
|
|
98
|
+
renderHook(
|
|
99
|
+
() =>
|
|
100
|
+
usePresenceForDocument({
|
|
101
|
+
documentId: 'movie-1',
|
|
102
|
+
documentType: 'movie',
|
|
103
|
+
perspective: 'published',
|
|
104
|
+
}),
|
|
105
|
+
{wrapper},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
expect(vi.mocked(getDocumentPresence).mock.calls[0][1]).toMatchObject({
|
|
109
|
+
documentId: 'movie-1',
|
|
110
|
+
perspective: 'published',
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('does not rebuild the source when a caller passes a fresh path each render', () => {
|
|
115
|
+
const {source} = createSource([])
|
|
116
|
+
vi.mocked(getDocumentPresence).mockReturnValue(source as never)
|
|
117
|
+
|
|
118
|
+
const {rerender} = renderHook(
|
|
119
|
+
// A new array identity every render, which is what real callers write.
|
|
120
|
+
() => usePresenceForDocument({documentId: 'movie-1', documentType: 'movie', path: ['title']}),
|
|
121
|
+
{wrapper},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
rerender()
|
|
125
|
+
rerender()
|
|
126
|
+
|
|
127
|
+
expect(vi.mocked(getDocumentPresence)).toHaveBeenCalledTimes(1)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('returns an empty array rather than undefined before anything arrives', () => {
|
|
131
|
+
const {source} = createSource(undefined as unknown as DocumentPresence[])
|
|
132
|
+
vi.mocked(getDocumentPresence).mockReturnValue(source as never)
|
|
133
|
+
|
|
134
|
+
const {result} = renderHook(
|
|
135
|
+
() => usePresenceForDocument({documentId: 'movie-1', documentType: 'movie'}),
|
|
136
|
+
{wrapper},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
expect(result.current.presence).toEqual([])
|
|
140
|
+
})
|
|
141
|
+
})
|