@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,244 @@
|
|
|
1
|
+
import {renderHook, waitFor} from '@testing-library/react'
|
|
2
|
+
import {afterEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
5
|
+
import {useWindowTitle} from './useWindowTitle'
|
|
6
|
+
|
|
7
|
+
vi.mock('../comlink/useWindowConnection', () => ({
|
|
8
|
+
useWindowConnection: vi.fn(),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
function createContextResponse(resource: Record<string, unknown>) {
|
|
12
|
+
return {context: {resource}}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('useWindowTitle', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.clearAllMocks()
|
|
18
|
+
document.title = ''
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should set the document title to the manifest title', async () => {
|
|
22
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
23
|
+
createContextResponse({
|
|
24
|
+
type: 'application',
|
|
25
|
+
title: 'sdk-movie-list',
|
|
26
|
+
manifest: {title: 'Movie List App'},
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
30
|
+
fetch: mockFetch,
|
|
31
|
+
sendMessage: vi.fn(),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
renderHook(() => useWindowTitle())
|
|
35
|
+
|
|
36
|
+
await waitFor(() => {
|
|
37
|
+
expect(document.title).toBe('Movie List App')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should prefer manifest title over system title', async () => {
|
|
42
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
43
|
+
createContextResponse({
|
|
44
|
+
type: 'application',
|
|
45
|
+
title: 'sdk-movie-list',
|
|
46
|
+
manifest: {title: 'Movie List App'},
|
|
47
|
+
activeDeployment: {manifest: {title: 'Deployed Title'}},
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
51
|
+
fetch: mockFetch,
|
|
52
|
+
sendMessage: vi.fn(),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
renderHook(() => useWindowTitle())
|
|
56
|
+
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(document.title).toBe('Movie List App')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should fall back to activeDeployment manifest title', async () => {
|
|
63
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
64
|
+
createContextResponse({
|
|
65
|
+
type: 'application',
|
|
66
|
+
title: 'sdk-movie-list',
|
|
67
|
+
manifest: null,
|
|
68
|
+
activeDeployment: {manifest: {title: 'Deployed Title'}},
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
72
|
+
fetch: mockFetch,
|
|
73
|
+
sendMessage: vi.fn(),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
renderHook(() => useWindowTitle())
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(document.title).toBe('Deployed Title')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should fall back to system title when no manifest title exists', async () => {
|
|
84
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
85
|
+
createContextResponse({
|
|
86
|
+
type: 'application',
|
|
87
|
+
title: 'sdk-movie-list',
|
|
88
|
+
manifest: null,
|
|
89
|
+
activeDeployment: null,
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
93
|
+
fetch: mockFetch,
|
|
94
|
+
sendMessage: vi.fn(),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
renderHook(() => useWindowTitle())
|
|
98
|
+
|
|
99
|
+
await waitFor(() => {
|
|
100
|
+
expect(document.title).toBe('sdk-movie-list')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should fall back to the iframe title when the dashboard resource has no title', async () => {
|
|
105
|
+
document.title = 'Local App'
|
|
106
|
+
|
|
107
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
108
|
+
createContextResponse({
|
|
109
|
+
type: 'application',
|
|
110
|
+
title: '',
|
|
111
|
+
manifest: null,
|
|
112
|
+
activeDeployment: null,
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
116
|
+
fetch: mockFetch,
|
|
117
|
+
sendMessage: vi.fn(),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const {rerender} = renderHook(({viewTitle}) => useWindowTitle(viewTitle), {
|
|
121
|
+
initialProps: {viewTitle: 'Teams'},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(document.title).toBe('Teams | Local App')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
rerender({viewTitle: 'Week 10'})
|
|
129
|
+
|
|
130
|
+
await waitFor(() => {
|
|
131
|
+
expect(document.title).toBe('Week 10 | Local App')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should prepend view title when provided', async () => {
|
|
136
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
137
|
+
createContextResponse({
|
|
138
|
+
type: 'application',
|
|
139
|
+
title: 'sdk-movie-list',
|
|
140
|
+
manifest: {title: 'Movie List App'},
|
|
141
|
+
}),
|
|
142
|
+
)
|
|
143
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
144
|
+
fetch: mockFetch,
|
|
145
|
+
sendMessage: vi.fn(),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
renderHook(() => useWindowTitle('Movies'))
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(document.title).toBe('Movies | Movie List App')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should update when view title changes', async () => {
|
|
156
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
157
|
+
createContextResponse({
|
|
158
|
+
type: 'application',
|
|
159
|
+
title: 'sdk-movie-list',
|
|
160
|
+
manifest: {title: 'Movie List App'},
|
|
161
|
+
}),
|
|
162
|
+
)
|
|
163
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
164
|
+
fetch: mockFetch,
|
|
165
|
+
sendMessage: vi.fn(),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const {rerender} = renderHook(({viewTitle}) => useWindowTitle(viewTitle), {
|
|
169
|
+
initialProps: {viewTitle: 'Movies'},
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await waitFor(() => {
|
|
173
|
+
expect(document.title).toBe('Movies | Movie List App')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
rerender({viewTitle: 'Movie Details'})
|
|
177
|
+
|
|
178
|
+
await waitFor(() => {
|
|
179
|
+
expect(document.title).toBe('Movie Details | Movie List App')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should restore the previous title on unmount', async () => {
|
|
184
|
+
document.title = 'Previous Title'
|
|
185
|
+
|
|
186
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
187
|
+
createContextResponse({
|
|
188
|
+
type: 'application',
|
|
189
|
+
title: 'sdk-movie-list',
|
|
190
|
+
manifest: {title: 'Movie List App'},
|
|
191
|
+
}),
|
|
192
|
+
)
|
|
193
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
194
|
+
fetch: mockFetch,
|
|
195
|
+
sendMessage: vi.fn(),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const {unmount} = renderHook(() => useWindowTitle('Movies'))
|
|
199
|
+
|
|
200
|
+
await waitFor(() => {
|
|
201
|
+
expect(document.title).toBe('Movies | Movie List App')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
unmount()
|
|
205
|
+
|
|
206
|
+
expect(document.title).toBe('Previous Title')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should handle AbortError silently', async () => {
|
|
210
|
+
const abortError = new Error('Aborted')
|
|
211
|
+
abortError.name = 'AbortError'
|
|
212
|
+
const mockFetch = vi.fn().mockRejectedValue(abortError)
|
|
213
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
214
|
+
fetch: mockFetch,
|
|
215
|
+
sendMessage: vi.fn(),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
renderHook(() => useWindowTitle())
|
|
219
|
+
|
|
220
|
+
await waitFor(() => {
|
|
221
|
+
expect(document.title).toBe('')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should log non-abort fetch errors', async () => {
|
|
226
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
227
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('Connection failed'))
|
|
228
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
229
|
+
fetch: mockFetch,
|
|
230
|
+
sendMessage: vi.fn(),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
renderHook(() => useWindowTitle('Movies'))
|
|
234
|
+
|
|
235
|
+
await waitFor(() => {
|
|
236
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
237
|
+
'Failed to fetch app title from dashboard context:',
|
|
238
|
+
expect.any(Error),
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
consoleSpy.mockRestore()
|
|
243
|
+
})
|
|
244
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
2
|
+
import {useEffect, useState} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
5
|
+
|
|
6
|
+
interface ContextResource {
|
|
7
|
+
type: string
|
|
8
|
+
title?: string
|
|
9
|
+
manifest?: {
|
|
10
|
+
title?: string
|
|
11
|
+
} | null
|
|
12
|
+
activeDeployment?: {
|
|
13
|
+
manifest?: {
|
|
14
|
+
title?: string
|
|
15
|
+
} | null
|
|
16
|
+
} | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ContextResponse {
|
|
20
|
+
context: {
|
|
21
|
+
resource: ContextResource
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveAppTitle(resource: ContextResource): string | undefined {
|
|
26
|
+
return resource.manifest?.title || resource.activeDeployment?.manifest?.title || resource.title
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sets the browser's document title, automatically including the app's name
|
|
31
|
+
* from the manifest.
|
|
32
|
+
*
|
|
33
|
+
* This follows the same convention as Sanity Studio workspaces, where the
|
|
34
|
+
* workspace name is always present in the title:
|
|
35
|
+
*
|
|
36
|
+
* - With a view title: `<viewTitle> | <appTitle>`
|
|
37
|
+
* - Without a view title: `<appTitle>`
|
|
38
|
+
*
|
|
39
|
+
* The Sanity dashboard appends `| Sanity` to produce the final browser tab title.
|
|
40
|
+
*
|
|
41
|
+
* @param viewTitle - An optional view-specific title to prepend to the app title.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* import {useWindowTitle} from '@sanity/sdk-react'
|
|
46
|
+
*
|
|
47
|
+
* function MoviesList() {
|
|
48
|
+
* useWindowTitle('Movies')
|
|
49
|
+
* return <div>...</div>
|
|
50
|
+
* }
|
|
51
|
+
*
|
|
52
|
+
* // Browser tab: "Movies | My App | Sanity"
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* // Call without arguments to show just the app title
|
|
58
|
+
* function AppRoot() {
|
|
59
|
+
* useWindowTitle()
|
|
60
|
+
* return <Outlet />
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* // Browser tab: "My App | Sanity"
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
export function useWindowTitle(viewTitle?: string): void {
|
|
69
|
+
const [appTitle, setAppTitle] = useState<string | null>(null)
|
|
70
|
+
|
|
71
|
+
const {fetch} = useWindowConnection({
|
|
72
|
+
name: SDK_NODE_NAME,
|
|
73
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!fetch) return
|
|
78
|
+
|
|
79
|
+
const controller = new AbortController()
|
|
80
|
+
|
|
81
|
+
async function fetchAppTitle(signal: AbortSignal) {
|
|
82
|
+
try {
|
|
83
|
+
const data = await fetch<ContextResponse>('dashboard/v1/context', undefined, {signal})
|
|
84
|
+
// Local development resources do not have a registered title or deployment manifest.
|
|
85
|
+
// In that case, use the title rendered into the iframe HTML by the Sanity CLI.
|
|
86
|
+
const title = resolveAppTitle(data.context.resource) || document.title
|
|
87
|
+
if (title) {
|
|
88
|
+
setAppTitle(title)
|
|
89
|
+
}
|
|
90
|
+
} catch (err: unknown) {
|
|
91
|
+
if (err instanceof Error && err.name === 'AbortError') return
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.error('Failed to fetch app title from dashboard context:', err)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fetchAppTitle(controller.signal)
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
controller.abort()
|
|
101
|
+
}
|
|
102
|
+
}, [fetch])
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!appTitle) return
|
|
106
|
+
|
|
107
|
+
const previous = document.title
|
|
108
|
+
document.title = viewTitle ? `${viewTitle} | ${appTitle}` : appTitle
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
document.title = previous
|
|
112
|
+
}
|
|
113
|
+
}, [viewTitle, appTitle])
|
|
114
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {type DatasetsResponse} from '@sanity/client'
|
|
2
|
+
import {
|
|
3
|
+
createSanityInstance,
|
|
4
|
+
getDatasetsState,
|
|
5
|
+
resolveDatasets,
|
|
6
|
+
type StateSource,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {type ReactNode} from 'react'
|
|
9
|
+
import {type Observable} from 'rxjs'
|
|
10
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
11
|
+
|
|
12
|
+
import {renderHook} from '../../../test/test-utils'
|
|
13
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
14
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
15
|
+
import {useDatasets} from './useDatasets'
|
|
16
|
+
|
|
17
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
18
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
19
|
+
return {...original, getDatasetsState: vi.fn(), resolveDatasets: vi.fn()}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const stateSource = (
|
|
23
|
+
current: DatasetsResponse | undefined,
|
|
24
|
+
): StateSource<DatasetsResponse | undefined> =>
|
|
25
|
+
({
|
|
26
|
+
getCurrent: vi.fn(() => current),
|
|
27
|
+
subscribe: vi.fn(),
|
|
28
|
+
get observable(): Observable<unknown> {
|
|
29
|
+
throw new Error('Not implemented')
|
|
30
|
+
},
|
|
31
|
+
}) as unknown as StateSource<DatasetsResponse | undefined>
|
|
32
|
+
|
|
33
|
+
const sanityInstance = expect.objectContaining({config: expect.any(Object)})
|
|
34
|
+
|
|
35
|
+
describe('useDatasets', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks()
|
|
38
|
+
vi.mocked(getDatasetsState).mockReturnValue(stateSource([] as unknown as DatasetsResponse))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('resolves the projectId from the instance config resource', () => {
|
|
42
|
+
// test-utils wraps with ResourceProvider projectId="test" dataset="test".
|
|
43
|
+
renderHook(() => useDatasets())
|
|
44
|
+
expect(getDatasetsState).toHaveBeenCalledWith(
|
|
45
|
+
sanityInstance,
|
|
46
|
+
expect.objectContaining({projectId: 'test'}),
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('lets an explicit projectId override the ambient resource', () => {
|
|
51
|
+
renderHook(() => useDatasets({projectId: 'explicit-project'}))
|
|
52
|
+
expect(getDatasetsState).toHaveBeenCalledWith(
|
|
53
|
+
sanityInstance,
|
|
54
|
+
expect.objectContaining({projectId: 'explicit-project'}),
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('resolves the projectId from an explicit resource when the config has none', () => {
|
|
59
|
+
renderHook(() => useDatasets(), {
|
|
60
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
61
|
+
<ResourceProvider
|
|
62
|
+
resource={{projectId: 'resource-project', dataset: 'production'}}
|
|
63
|
+
fallback={null}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</ResourceProvider>
|
|
67
|
+
),
|
|
68
|
+
})
|
|
69
|
+
expect(getDatasetsState).toHaveBeenCalledWith(
|
|
70
|
+
sanityInstance,
|
|
71
|
+
expect.objectContaining({projectId: 'resource-project'}),
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('resolves a dataset-less projectId config for project-scoped use', () => {
|
|
76
|
+
renderHook(() => useDatasets(), {
|
|
77
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
78
|
+
<ResourceProvider projectId="config-project" fallback={null}>
|
|
79
|
+
{children}
|
|
80
|
+
</ResourceProvider>
|
|
81
|
+
),
|
|
82
|
+
})
|
|
83
|
+
// A dataset-less config can't form a DatasetResource; the projectId is carried
|
|
84
|
+
// via ProjectContext and injected so project-scoped reads still resolve it.
|
|
85
|
+
expect(getDatasetsState).toHaveBeenCalledWith(
|
|
86
|
+
sanityInstance,
|
|
87
|
+
expect.objectContaining({projectId: 'config-project'}),
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('resolves a bare projectId from a ResourceProvider when the parent instance has no config', () => {
|
|
92
|
+
// An instance with no project/dataset config and no ambient
|
|
93
|
+
// resource, then a projectId-only ResourceProvider.
|
|
94
|
+
const emptyInstance = createSanityInstance({})
|
|
95
|
+
renderHook(() => useDatasets(), {
|
|
96
|
+
wrapper: ({children}: {children: ReactNode}) => (
|
|
97
|
+
<SanityInstanceContext.Provider value={emptyInstance}>
|
|
98
|
+
<ResourceProvider projectId="bare-project" fallback={null}>
|
|
99
|
+
{children}
|
|
100
|
+
</ResourceProvider>
|
|
101
|
+
</SanityInstanceContext.Provider>
|
|
102
|
+
),
|
|
103
|
+
})
|
|
104
|
+
expect(getDatasetsState).toHaveBeenCalledWith(
|
|
105
|
+
emptyInstance,
|
|
106
|
+
expect.objectContaining({projectId: 'bare-project'}),
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('suspends via resolveDatasets until dataset data is available', () => {
|
|
111
|
+
vi.mocked(getDatasetsState).mockReturnValue(stateSource(undefined))
|
|
112
|
+
vi.mocked(resolveDatasets).mockReturnValue(new Promise(() => {}))
|
|
113
|
+
renderHook(() => useDatasets())
|
|
114
|
+
expect(resolveDatasets).toHaveBeenCalled()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import {type DatasetsResponse} from '@sanity/client'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getDatasetsState,
|
|
4
|
+
type ProjectHandle,
|
|
5
|
+
resolveDatasets,
|
|
6
|
+
type SanityInstance,
|
|
7
|
+
type StateSource,
|
|
8
|
+
} from '@sanity/sdk'
|
|
3
9
|
|
|
4
10
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
11
|
+
import {useResolvedProjectId} from '../helpers/useResolvedProjectId'
|
|
5
12
|
|
|
6
13
|
type UseDatasets = {
|
|
7
14
|
/**
|
|
@@ -9,12 +16,13 @@ type UseDatasets = {
|
|
|
9
16
|
* Returns metadata for each dataset the current user has access to.
|
|
10
17
|
*
|
|
11
18
|
* @category Datasets
|
|
12
|
-
* @param options -
|
|
13
|
-
*
|
|
19
|
+
* @param options - Optional project/resource to read datasets for. Defaults to
|
|
20
|
+
* the resource named in `ResourceProvider`/`SDKProvider`.
|
|
21
|
+
* @returns The metadata for your the datasets
|
|
14
22
|
*
|
|
15
23
|
* @example
|
|
16
24
|
* ```tsx
|
|
17
|
-
* const datasets = useDatasets(
|
|
25
|
+
* const datasets = useDatasets()
|
|
18
26
|
*
|
|
19
27
|
* return (
|
|
20
28
|
* <select>
|
|
@@ -25,20 +33,32 @@ type UseDatasets = {
|
|
|
25
33
|
* )
|
|
26
34
|
* ```
|
|
27
35
|
*
|
|
36
|
+
* @remarks
|
|
37
|
+
* The `projectId` is resolved in order from:
|
|
38
|
+
* 1. an explicit `projectId` option
|
|
39
|
+
* 2. A legacy ProjectContext (e.g. a `<ResourceProvider projectId="…">` with no dataset), then
|
|
40
|
+
* 3. The active resource (`ResourceProvider`/`SDKProvider`)
|
|
41
|
+
* 4. `instance.config`.
|
|
28
42
|
*/
|
|
29
|
-
(options
|
|
43
|
+
(options?: ProjectHandle): DatasetsResponse
|
|
30
44
|
}
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
* @public
|
|
34
|
-
* @function
|
|
35
|
-
*/
|
|
36
|
-
export const useDatasets: UseDatasets = createStateSourceHook({
|
|
46
|
+
const useDatasetsBase = createStateSourceHook({
|
|
37
47
|
getState: getDatasetsState as (
|
|
38
48
|
instance: SanityInstance,
|
|
39
|
-
|
|
49
|
+
projectHandle?: ProjectHandle,
|
|
40
50
|
) => StateSource<DatasetsResponse>,
|
|
41
|
-
shouldSuspend: (instance,
|
|
42
|
-
|
|
51
|
+
shouldSuspend: (instance, projectHandle?: ProjectHandle) =>
|
|
52
|
+
// remove `undefined` since we're suspending when that is the case
|
|
53
|
+
getDatasetsState(instance, projectHandle).getCurrent() === undefined,
|
|
43
54
|
suspender: resolveDatasets,
|
|
44
55
|
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @public
|
|
59
|
+
* @function
|
|
60
|
+
*/
|
|
61
|
+
export const useDatasets: UseDatasets = (options) => {
|
|
62
|
+
const projectId = useResolvedProjectId(options)
|
|
63
|
+
return useDatasetsBase(projectId ? {...options, projectId} : options)
|
|
64
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {applyDocumentActions, createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {describe, it} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {renderHook} from '../../../test/test-utils'
|
|
5
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
7
|
+
|
|
8
|
+
// Resource resolution, mismatch detection, and context fallback are covered
|
|
9
|
+
// by hooks/helpers/useApplyActions.test.tsx — both this hook and
|
|
10
|
+
// useApplyReleaseActions are typed wrappers over that shared implementation.
|
|
11
|
+
// These tests just verify the wrapper forwards document actions through.
|
|
12
|
+
|
|
13
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
14
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
15
|
+
return {...original, applyDocumentActions: vi.fn()}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
vi.mock('../context/useSanityInstance')
|
|
19
|
+
|
|
20
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
21
|
+
|
|
22
|
+
describe('useApplyDocumentActions', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.resetAllMocks()
|
|
25
|
+
vi.mocked(useSanityInstance).mockReturnValueOnce(instance)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('forwards a document action to applyDocumentActions with the resolved resource', () => {
|
|
29
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
30
|
+
result.current({
|
|
31
|
+
type: 'document.edit',
|
|
32
|
+
documentType: 'post',
|
|
33
|
+
documentId: 'abc',
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
37
|
+
actions: [
|
|
38
|
+
{
|
|
39
|
+
type: 'document.edit',
|
|
40
|
+
documentType: 'post',
|
|
41
|
+
documentId: 'abc',
|
|
42
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|