@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,242 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Comment,
|
|
3
|
+
type CommentsOptions,
|
|
4
|
+
getCommentsState,
|
|
5
|
+
resolveComments,
|
|
6
|
+
type StateSource,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {act, render, screen} from '@testing-library/react'
|
|
9
|
+
import {Suspense} from 'react'
|
|
10
|
+
import {type Observable, Subject} from 'rxjs'
|
|
11
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
12
|
+
|
|
13
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
14
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
15
|
+
import {useComments} from './useComments'
|
|
16
|
+
|
|
17
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
18
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
19
|
+
return {...original, getCommentsState: vi.fn(), resolveComments: vi.fn()}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const HANDLE = {documentId: 'doc-1', documentType: 'author'}
|
|
23
|
+
|
|
24
|
+
function comment(id: string): Comment {
|
|
25
|
+
return {
|
|
26
|
+
id,
|
|
27
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
28
|
+
authorId: 'user-1',
|
|
29
|
+
message: null,
|
|
30
|
+
threadId: 'thread-1',
|
|
31
|
+
status: 'open',
|
|
32
|
+
documentId: 'doc-1',
|
|
33
|
+
documentType: 'author',
|
|
34
|
+
fieldPath: '',
|
|
35
|
+
reactions: [],
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Stands in for the store, one source per option set so a test can hold one
|
|
41
|
+
* document loaded and another not.
|
|
42
|
+
*
|
|
43
|
+
* `getCurrent` must hand back the same array every call for a given option set.
|
|
44
|
+
* Returning a fresh one sends `useSyncExternalStore` into a render loop, which
|
|
45
|
+
* is why the store memoises its selectors.
|
|
46
|
+
*/
|
|
47
|
+
function mockSource(
|
|
48
|
+
getCurrent: (options: CommentsOptions) => Comment[] | undefined,
|
|
49
|
+
changed$?: Subject<void>,
|
|
50
|
+
) {
|
|
51
|
+
vi.mocked(getCommentsState).mockImplementation(
|
|
52
|
+
(_instance, options) =>
|
|
53
|
+
({
|
|
54
|
+
getCurrent: () => getCurrent(options),
|
|
55
|
+
subscribe: vi.fn((cb?: () => void) => {
|
|
56
|
+
const subscription = changed$?.subscribe(() => cb?.())
|
|
57
|
+
return () => subscription?.unsubscribe()
|
|
58
|
+
}),
|
|
59
|
+
get observable(): Observable<Comment[] | undefined> {
|
|
60
|
+
throw new Error('Not implemented')
|
|
61
|
+
},
|
|
62
|
+
}) as StateSource<Comment[] | undefined>,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Hoisted so the context value stays identical across renders. */
|
|
67
|
+
const RELEASE_PERSPECTIVE = {releaseName: 'summer'}
|
|
68
|
+
|
|
69
|
+
function Wrapper({children}: {children: React.ReactNode}) {
|
|
70
|
+
return (
|
|
71
|
+
<ResourceProvider projectId="p" dataset="d" fallback={<p>Loading…</p>}>
|
|
72
|
+
<ResourcesContext.Provider value={{other: {projectId: 'p2', dataset: 'd2'}}}>
|
|
73
|
+
<Suspense fallback={<p data-testid="suspended">Suspended</p>}>{children}</Suspense>
|
|
74
|
+
</ResourcesContext.Provider>
|
|
75
|
+
</ResourceProvider>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function PerspectiveWrapper({children}: {children: React.ReactNode}) {
|
|
80
|
+
return (
|
|
81
|
+
<ResourceProvider
|
|
82
|
+
projectId="p"
|
|
83
|
+
dataset="d"
|
|
84
|
+
perspective={RELEASE_PERSPECTIVE}
|
|
85
|
+
fallback={<p>Loading…</p>}
|
|
86
|
+
>
|
|
87
|
+
<Suspense fallback={<p data-testid="suspended">Suspended</p>}>{children}</Suspense>
|
|
88
|
+
</ResourceProvider>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe('useComments', () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
vi.resetAllMocks()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('renders the comments once they are loaded', () => {
|
|
98
|
+
const loaded = [comment('a'), comment('b')]
|
|
99
|
+
mockSource(() => loaded)
|
|
100
|
+
|
|
101
|
+
function TestComponent() {
|
|
102
|
+
const {comments, isPending} = useComments(HANDLE)
|
|
103
|
+
return <div data-testid="out">{`${comments.length} ${isPending ? 'pending' : 'idle'}`}</div>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
render(<TestComponent />, {wrapper: Wrapper})
|
|
107
|
+
|
|
108
|
+
expect(screen.getByTestId('out').textContent).toBe('2 idle')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('suspends until the first snapshot arrives', async () => {
|
|
112
|
+
const loaded = [comment('a')]
|
|
113
|
+
const ref: {current: Comment[] | undefined} = {current: undefined}
|
|
114
|
+
const changed$ = new Subject<void>()
|
|
115
|
+
mockSource(() => ref.current, changed$)
|
|
116
|
+
|
|
117
|
+
let settle: () => void = () => {}
|
|
118
|
+
vi.mocked(resolveComments).mockReturnValue(
|
|
119
|
+
new Promise<Comment[]>((resolve) => {
|
|
120
|
+
settle = () => resolve(loaded)
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
function TestComponent() {
|
|
125
|
+
const {comments} = useComments(HANDLE)
|
|
126
|
+
return <div data-testid="out">{comments.length}</div>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
render(<TestComponent />, {wrapper: Wrapper})
|
|
130
|
+
expect(screen.getByTestId('suspended')).toBeInTheDocument()
|
|
131
|
+
|
|
132
|
+
await act(async () => {
|
|
133
|
+
ref.current = loaded
|
|
134
|
+
settle()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
expect(screen.getByTestId('out').textContent).toBe('1')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('passes the field path and status through to the store', () => {
|
|
141
|
+
const loaded: Comment[] = []
|
|
142
|
+
mockSource(() => loaded)
|
|
143
|
+
|
|
144
|
+
function TestComponent() {
|
|
145
|
+
useComments({...HANDLE, fieldPath: ['body', {_key: 'intro'}], status: 'resolved'})
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
render(<TestComponent />, {wrapper: Wrapper})
|
|
150
|
+
|
|
151
|
+
expect(getCommentsState).toHaveBeenCalledWith(
|
|
152
|
+
expect.anything(),
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
documentId: 'doc-1',
|
|
155
|
+
fieldPath: 'body[_key=="intro"]',
|
|
156
|
+
status: 'resolved',
|
|
157
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('resolves a named resource from context', () => {
|
|
163
|
+
const loaded: Comment[] = []
|
|
164
|
+
mockSource(() => loaded)
|
|
165
|
+
|
|
166
|
+
function TestComponent() {
|
|
167
|
+
useComments({...HANDLE, resourceName: 'other'})
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
render(<TestComponent />, {wrapper: Wrapper})
|
|
172
|
+
|
|
173
|
+
expect(getCommentsState).toHaveBeenCalledWith(
|
|
174
|
+
expect.anything(),
|
|
175
|
+
expect.objectContaining({resource: {projectId: 'p2', dataset: 'd2'}}),
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('fills in the perspective from context', () => {
|
|
180
|
+
// Core turns a release perspective into `target.documentVersionId` and into
|
|
181
|
+
// the key the list is stored under, so dropping it here would read and
|
|
182
|
+
// write the wrong release with nothing to show that anything went wrong.
|
|
183
|
+
const loaded: Comment[] = []
|
|
184
|
+
mockSource(() => loaded)
|
|
185
|
+
|
|
186
|
+
function TestComponent() {
|
|
187
|
+
useComments(HANDLE)
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
render(<TestComponent />, {wrapper: PerspectiveWrapper})
|
|
192
|
+
|
|
193
|
+
expect(getCommentsState).toHaveBeenCalledWith(
|
|
194
|
+
expect.anything(),
|
|
195
|
+
expect.objectContaining({perspective: RELEASE_PERSPECTIVE}),
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('keeps the previous list on screen while a different document loads', async () => {
|
|
200
|
+
const first = [comment('a')]
|
|
201
|
+
const second = [comment('b'), comment('c')]
|
|
202
|
+
// Only `doc-1` is loaded, so switching to `doc-2` has to suspend.
|
|
203
|
+
const byDocument: Record<string, Comment[] | undefined> = {'doc-1': first}
|
|
204
|
+
mockSource((options) => byDocument[options.documentId])
|
|
205
|
+
|
|
206
|
+
let settle: () => void = () => {}
|
|
207
|
+
vi.mocked(resolveComments).mockReturnValue(
|
|
208
|
+
new Promise<Comment[]>((resolve) => {
|
|
209
|
+
settle = () => resolve(second)
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
function TestComponent({documentId}: {documentId: string}) {
|
|
214
|
+
const {comments, isPending} = useComments({...HANDLE, documentId})
|
|
215
|
+
return <div data-testid="out">{`${comments.length} ${isPending ? 'pending' : 'idle'}`}</div>
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const {rerender} = render(<TestComponent documentId="doc-1" />, {wrapper: Wrapper})
|
|
219
|
+
expect(screen.getByTestId('out').textContent).toBe('1 idle')
|
|
220
|
+
|
|
221
|
+
await act(async () => {
|
|
222
|
+
rerender(<TestComponent documentId="doc-2" />)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// The swap happens inside a transition, so the render that suspends is
|
|
226
|
+
// thrown away rather than falling back: `doc-1` stays on screen and
|
|
227
|
+
// `isPending` is what reports the switch.
|
|
228
|
+
expect(screen.queryByTestId('suspended')).not.toBeInTheDocument()
|
|
229
|
+
expect(screen.getByTestId('out').textContent).toBe('1 pending')
|
|
230
|
+
expect(getCommentsState).toHaveBeenCalledWith(
|
|
231
|
+
expect.anything(),
|
|
232
|
+
expect.objectContaining({documentId: 'doc-2'}),
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
await act(async () => {
|
|
236
|
+
byDocument['doc-2'] = second
|
|
237
|
+
settle()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(screen.getByTestId('out').textContent).toBe('2 idle')
|
|
241
|
+
})
|
|
242
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {type Comment, type CommentsOptions, getCommentsState, resolveComments} from '@sanity/sdk'
|
|
2
|
+
import {useMemo} from 'react'
|
|
3
|
+
|
|
4
|
+
import {type WithResourceNameSupport} from '../helpers/useNormalizedResourceOptions'
|
|
5
|
+
import {type CommentListSource, useCommentList} from './useCommentList'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @public
|
|
9
|
+
* @category Types
|
|
10
|
+
*/
|
|
11
|
+
export interface UseCommentsResult {
|
|
12
|
+
/** Every matching comment, newest first, replies included. */
|
|
13
|
+
comments: Comment[]
|
|
14
|
+
/** True while switching to a different document or filter. */
|
|
15
|
+
isPending: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SOURCE: CommentListSource<Comment[]> = {
|
|
19
|
+
getState: getCommentsState,
|
|
20
|
+
resolve: resolveComments,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reads a document's comments and keeps them up to date.
|
|
25
|
+
*
|
|
26
|
+
* Comments are shared with the Studio: they live in the project's comments
|
|
27
|
+
* dataset, so a thread started here shows up there and the other way round. The
|
|
28
|
+
* list is flat, replies included; reach for {@link useCommentThreads} to read it
|
|
29
|
+
* grouped.
|
|
30
|
+
*
|
|
31
|
+
* Suspends until the comments have loaded. Switching document or filter is a
|
|
32
|
+
* transition, so the previous list stays on screen and `isPending` goes true
|
|
33
|
+
* rather than the component suspending again.
|
|
34
|
+
*
|
|
35
|
+
* @category Comments
|
|
36
|
+
* @function
|
|
37
|
+
* @param options - The document to read, optionally narrowed by `fieldPath` or `status`
|
|
38
|
+
* @returns The matching comments, and whether a switch is in flight
|
|
39
|
+
*
|
|
40
|
+
* @example Count the open threads on a field
|
|
41
|
+
* ```tsx
|
|
42
|
+
* function TitleCommentCount({documentId}: {documentId: string}) {
|
|
43
|
+
* const {comments} = useComments({
|
|
44
|
+
* documentId,
|
|
45
|
+
* documentType: 'article',
|
|
46
|
+
* fieldPath: 'title',
|
|
47
|
+
* status: 'open',
|
|
48
|
+
* })
|
|
49
|
+
*
|
|
50
|
+
* return <span>{comments.length}</span>
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @public
|
|
55
|
+
*/
|
|
56
|
+
export function useComments(options: WithResourceNameSupport<CommentsOptions>): UseCommentsResult {
|
|
57
|
+
const {value, isPending} = useCommentList('useComments', options, SOURCE)
|
|
58
|
+
return useMemo(() => ({comments: value, isPending}), [isPending, value])
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {renderHook as reactRenderHook} from '@testing-library/react'
|
|
2
|
+
import {type ReactNode} from 'react'
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {renderHook} from '../../../test/test-utils'
|
|
6
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
7
|
+
import {useResource} from './useResource'
|
|
8
|
+
|
|
9
|
+
describe('useResource', () => {
|
|
10
|
+
it('returns the resource from the instance config when no explicit resource is set', () => {
|
|
11
|
+
// test-utils wraps with ResourceProvider projectId="test" dataset="test"
|
|
12
|
+
const {result} = renderHook(() => useResource())
|
|
13
|
+
expect(result.current).toEqual({projectId: 'test', dataset: 'test'})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns the explicit resource when ResourceProvider has a resource prop', () => {
|
|
17
|
+
const resource = {projectId: 'explicit-project', dataset: 'explicit-dataset'}
|
|
18
|
+
const {result} = reactRenderHook(() => useResource(), {
|
|
19
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
20
|
+
<ResourceProvider resource={resource} fallback={null}>
|
|
21
|
+
{children}
|
|
22
|
+
</ResourceProvider>
|
|
23
|
+
),
|
|
24
|
+
})
|
|
25
|
+
expect(result.current).toEqual(resource)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns undefined when no resource or instance config is available', () => {
|
|
29
|
+
const {result} = reactRenderHook(() => useResource())
|
|
30
|
+
expect(result.current).toBeUndefined()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {type DocumentResource} from '@sanity/sdk'
|
|
2
|
+
|
|
3
|
+
import {useEffectiveContextResource} from '../helpers/useNormalizedResourceOptions'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the currently active `DocumentResource` for the nearest resource context.
|
|
7
|
+
*
|
|
8
|
+
* Resolves in priority order:
|
|
9
|
+
* 1. A `resource` prop on the nearest `<ResourceProvider>`
|
|
10
|
+
* 2. The `projectId`/`dataset` from the current `SanityInstance` config
|
|
11
|
+
* 3. `undefined` when neither is available
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
* @category Platform
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const resource = useResource()
|
|
19
|
+
* console.log(resource?.projectId, resource?.dataset)
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function useResource(): DocumentResource | undefined {
|
|
23
|
+
return useEffectiveContextResource()
|
|
24
|
+
}
|
|
@@ -16,20 +16,37 @@ describe('useSanityInstance', () => {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
it('should return the Sanity instance from context', () => {
|
|
19
|
-
|
|
19
|
+
// Create a Sanity instance
|
|
20
|
+
const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
|
|
20
21
|
|
|
22
|
+
// Render the hook with the wrapper that provides the context
|
|
21
23
|
const {result} = renderHook(() => useSanityInstance(), {
|
|
22
24
|
wrapper: createWrapper(instance),
|
|
23
25
|
})
|
|
24
26
|
|
|
27
|
+
// Check that the correct instance is returned
|
|
25
28
|
expect(result.current).toBe(instance)
|
|
26
29
|
})
|
|
27
30
|
|
|
28
31
|
it('should throw an error if no instance is found in context', () => {
|
|
32
|
+
// Expect the hook to throw when no instance is in context
|
|
29
33
|
expect(() => {
|
|
30
34
|
renderHook(() => useSanityInstance(), {
|
|
31
35
|
wrapper: createWrapper(null),
|
|
32
36
|
})
|
|
33
37
|
}).toThrow('SanityInstance context not found')
|
|
34
38
|
})
|
|
39
|
+
|
|
40
|
+
it('should return the current instance when no config is provided', () => {
|
|
41
|
+
// Create a Sanity instance
|
|
42
|
+
const instance = createSanityInstance({projectId: 'gp', dataset: 'gp-ds'})
|
|
43
|
+
|
|
44
|
+
// Render the hook with the wrapper that provides the context
|
|
45
|
+
const {result} = renderHook(() => useSanityInstance(), {
|
|
46
|
+
wrapper: createWrapper(instance),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Should return the instance
|
|
50
|
+
expect(result.current).toBe(instance)
|
|
51
|
+
})
|
|
35
52
|
})
|
|
@@ -4,21 +4,21 @@ import {useContext} from 'react'
|
|
|
4
4
|
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Retrieves the current Sanity instance from
|
|
7
|
+
* Retrieves the current Sanity instance from context
|
|
8
8
|
*
|
|
9
9
|
* @public
|
|
10
10
|
*
|
|
11
11
|
* @category Platform
|
|
12
|
-
* @returns The current instance
|
|
12
|
+
* @returns The current Sanity instance
|
|
13
13
|
*
|
|
14
14
|
* @remarks
|
|
15
|
-
* This hook accesses the nearest Sanity instance from React context
|
|
16
|
-
*
|
|
15
|
+
* This hook accesses the nearest Sanity instance from the React context.
|
|
16
|
+
* The hook must be used within a component wrapped by a `ResourceProvider` or `SanityApp`.
|
|
17
17
|
*
|
|
18
18
|
* @example Get the current instance
|
|
19
19
|
* ```tsx
|
|
20
20
|
* const instance = useSanityInstance()
|
|
21
|
-
* console.log(instance.config)
|
|
21
|
+
* console.log(instance.config.projectId)
|
|
22
22
|
* ```
|
|
23
23
|
*
|
|
24
24
|
* @throws Error if no SanityInstance is found in context
|
|
@@ -28,7 +28,7 @@ export const useSanityInstance = (): SanityInstance => {
|
|
|
28
28
|
|
|
29
29
|
if (!instance) {
|
|
30
30
|
throw new Error(
|
|
31
|
-
|
|
31
|
+
`SanityInstance context not found. Please ensure that your component is wrapped in a ResourceProvider or a SanityApp component.`,
|
|
32
32
|
)
|
|
33
33
|
}
|
|
34
34
|
|