@sanity/sdk-react 2.8.0 → 2.9.0
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/dist/index.d.ts +144 -25
- package/dist/index.js +216 -122
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/_exports/sdk-react.ts +1 -0
- package/src/components/SanityApp.tsx +1 -0
- package/src/context/ResourceProvider.test.tsx +7 -1
- package/src/context/ResourceProvider.tsx +6 -0
- package/src/context/SDKStudioContext.ts +6 -0
- package/src/hooks/dashboard/useDispatchIntent.test.ts +2 -0
- package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
- package/src/hooks/dashboard/useWindowTitle.ts +112 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
- package/src/hooks/document/useApplyDocumentActions.ts +99 -3
- package/src/hooks/document/useDocument.ts +22 -6
- package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
- package/src/hooks/document/useDocumentEvent.ts +10 -3
- package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
- package/src/hooks/document/useDocumentPermissions.ts +22 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
- package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
- package/src/hooks/document/useEditDocument.ts +34 -8
- package/src/hooks/documents/useDocuments.ts +2 -0
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +50 -28
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +2 -0
- package/src/hooks/presence/usePresence.ts +2 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
- package/src/hooks/preview/useDocumentPreview.tsx +39 -55
- package/src/hooks/projection/useDocumentProjection.ts +2 -0
- package/src/hooks/query/useQuery.ts +2 -0
- package/src/hooks/releases/useActiveReleases.ts +32 -13
- package/src/hooks/releases/usePerspective.ts +26 -14
- package/src/hooks/users/useUser.ts +2 -0
- package/src/hooks/users/useUsers.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"react-compiler-runtime": "19.1.0-rc.2",
|
|
53
53
|
"react-error-boundary": "^5.0.0",
|
|
54
54
|
"rxjs": "^7.8.2",
|
|
55
|
-
"@sanity/sdk": "2.
|
|
55
|
+
"@sanity/sdk": "2.9.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@sanity/browserslist-config": "^1.0.5",
|
|
@@ -75,13 +75,13 @@
|
|
|
75
75
|
"react-dom": "^19.2.1",
|
|
76
76
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
77
77
|
"typescript": "^5.8.3",
|
|
78
|
-
"vite": "^
|
|
78
|
+
"vite": "^7.0.0",
|
|
79
79
|
"vitest": "^3.2.4",
|
|
80
|
-
"@repo/
|
|
81
|
-
"@repo/config-eslint": "0.0.0",
|
|
82
|
-
"@repo/config-test": "0.0.1",
|
|
80
|
+
"@repo/tsconfig": "0.0.1",
|
|
83
81
|
"@repo/package.config": "0.0.1",
|
|
84
|
-
"@repo/
|
|
82
|
+
"@repo/config-test": "0.0.1",
|
|
83
|
+
"@repo/package.bundle": "3.82.0",
|
|
84
|
+
"@repo/config-eslint": "0.0.0"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -50,6 +50,7 @@ export {
|
|
|
50
50
|
} from '../hooks/dashboard/useNavigateToStudioDocument'
|
|
51
51
|
export {useRecordDocumentHistoryEvent} from '../hooks/dashboard/useRecordDocumentHistoryEvent'
|
|
52
52
|
export {useStudioWorkspacesByProjectIdDataset} from '../hooks/dashboard/useStudioWorkspacesByProjectIdDataset'
|
|
53
|
+
export {useWindowTitle} from '../hooks/dashboard/useWindowTitle'
|
|
53
54
|
export {useDatasets} from '../hooks/datasets/useDatasets'
|
|
54
55
|
export {useApplyDocumentActions} from '../hooks/document/useApplyDocumentActions'
|
|
55
56
|
export {useDocument} from '../hooks/document/useDocument'
|
|
@@ -37,6 +37,7 @@ function deriveConfigFromWorkspace(workspace: StudioWorkspaceHandle): SanityConf
|
|
|
37
37
|
projectId: workspace.projectId,
|
|
38
38
|
dataset: workspace.dataset,
|
|
39
39
|
studio: {
|
|
40
|
+
authenticated: workspace.authenticated,
|
|
40
41
|
auth: workspace.auth.token ? {token: workspace.auth.token} : undefined,
|
|
41
42
|
},
|
|
42
43
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {act, render, screen} from '@testing-library/react'
|
|
3
3
|
import {StrictMode, use, useEffect} from 'react'
|
|
4
|
-
import {describe, expect, it} from 'vitest'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
5
|
|
|
6
6
|
import {ResourceProvider} from './ResourceProvider'
|
|
7
7
|
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
@@ -37,6 +37,7 @@ describe('ResourceProvider', () => {
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
it('shows fallback during loading', async () => {
|
|
40
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
40
41
|
const {promise, resolve} = promiseWithResolvers()
|
|
41
42
|
function SuspendingChild(): React.ReactNode {
|
|
42
43
|
throw promise
|
|
@@ -52,6 +53,8 @@ describe('ResourceProvider', () => {
|
|
|
52
53
|
act(() => {
|
|
53
54
|
resolve()
|
|
54
55
|
})
|
|
56
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
57
|
+
consoleSpy.mockRestore()
|
|
55
58
|
})
|
|
56
59
|
|
|
57
60
|
it('creates root instance when no parent context exists', async () => {
|
|
@@ -141,6 +144,7 @@ describe('ResourceProvider', () => {
|
|
|
141
144
|
})
|
|
142
145
|
|
|
143
146
|
it('uses default fallback when none provided', async () => {
|
|
147
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
144
148
|
const {promise, resolve} = promiseWithResolvers()
|
|
145
149
|
function SuspendingChild(): React.ReactNode {
|
|
146
150
|
throw promise
|
|
@@ -157,5 +161,7 @@ describe('ResourceProvider', () => {
|
|
|
157
161
|
act(() => {
|
|
158
162
|
resolve()
|
|
159
163
|
})
|
|
164
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
165
|
+
consoleSpy.mockRestore()
|
|
160
166
|
})
|
|
161
167
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {initTelemetry} from '@sanity/sdk/_internal'
|
|
2
3
|
import {Suspense, useContext, useEffect, useMemo, useRef} from 'react'
|
|
3
4
|
|
|
4
5
|
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
@@ -78,6 +79,11 @@ export function ResourceProvider({
|
|
|
78
79
|
[config, parent],
|
|
79
80
|
)
|
|
80
81
|
|
|
82
|
+
const projectId = config.projectId ?? ''
|
|
83
|
+
useMemo(() => {
|
|
84
|
+
if (projectId && !parent) initTelemetry(instance, projectId)
|
|
85
|
+
}, [instance, projectId, parent])
|
|
86
|
+
|
|
81
87
|
// Ref to hold the scheduled disposal timer.
|
|
82
88
|
const disposal = useRef<{
|
|
83
89
|
instance: SanityInstance
|
|
@@ -13,6 +13,12 @@ export interface StudioWorkspaceHandle {
|
|
|
13
13
|
projectId: string
|
|
14
14
|
/** The dataset name for this workspace. */
|
|
15
15
|
dataset: string
|
|
16
|
+
/**
|
|
17
|
+
* Whether the Studio has determined the user is authenticated.
|
|
18
|
+
* When `true` and the token source emits `null`, the SDK infers
|
|
19
|
+
* cookie-based auth is in use and skips the logged-out state.
|
|
20
|
+
*/
|
|
21
|
+
authenticated?: boolean
|
|
16
22
|
/** Authentication state for this workspace. */
|
|
17
23
|
auth: {
|
|
18
24
|
/**
|
|
@@ -37,11 +37,13 @@ describe('useDispatchIntent', () => {
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
it('should throw error when neither action nor intentId is provided', () => {
|
|
40
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
40
41
|
const {result} = renderHook(() => useDispatchIntent({documentHandle: mockDocumentHandle}))
|
|
41
42
|
|
|
42
43
|
expect(() => result.current.dispatchIntent()).toThrow(
|
|
43
44
|
'useDispatchIntent: Either `action` or `intentId` must be provided.',
|
|
44
45
|
)
|
|
46
|
+
consoleErrorSpy.mockRestore()
|
|
45
47
|
})
|
|
46
48
|
|
|
47
49
|
it('should handle errors gracefully', () => {
|
|
@@ -0,0 +1,213 @@
|
|
|
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 prepend view title when provided', async () => {
|
|
105
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
106
|
+
createContextResponse({
|
|
107
|
+
type: 'application',
|
|
108
|
+
title: 'sdk-movie-list',
|
|
109
|
+
manifest: {title: 'Movie List App'},
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
112
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
113
|
+
fetch: mockFetch,
|
|
114
|
+
sendMessage: vi.fn(),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
renderHook(() => useWindowTitle('Movies'))
|
|
118
|
+
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(document.title).toBe('Movies | Movie List App')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should update when view title changes', async () => {
|
|
125
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
126
|
+
createContextResponse({
|
|
127
|
+
type: 'application',
|
|
128
|
+
title: 'sdk-movie-list',
|
|
129
|
+
manifest: {title: 'Movie List App'},
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
133
|
+
fetch: mockFetch,
|
|
134
|
+
sendMessage: vi.fn(),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const {rerender} = renderHook(({viewTitle}) => useWindowTitle(viewTitle), {
|
|
138
|
+
initialProps: {viewTitle: 'Movies'},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(document.title).toBe('Movies | Movie List App')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
rerender({viewTitle: 'Movie Details'})
|
|
146
|
+
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
expect(document.title).toBe('Movie Details | Movie List App')
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should restore the previous title on unmount', async () => {
|
|
153
|
+
document.title = 'Previous Title'
|
|
154
|
+
|
|
155
|
+
const mockFetch = vi.fn().mockResolvedValue(
|
|
156
|
+
createContextResponse({
|
|
157
|
+
type: 'application',
|
|
158
|
+
title: 'sdk-movie-list',
|
|
159
|
+
manifest: {title: 'Movie List App'},
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
163
|
+
fetch: mockFetch,
|
|
164
|
+
sendMessage: vi.fn(),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const {unmount} = renderHook(() => useWindowTitle('Movies'))
|
|
168
|
+
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
expect(document.title).toBe('Movies | Movie List App')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
unmount()
|
|
174
|
+
|
|
175
|
+
expect(document.title).toBe('Previous Title')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should handle AbortError silently', async () => {
|
|
179
|
+
const abortError = new Error('Aborted')
|
|
180
|
+
abortError.name = 'AbortError'
|
|
181
|
+
const mockFetch = vi.fn().mockRejectedValue(abortError)
|
|
182
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
183
|
+
fetch: mockFetch,
|
|
184
|
+
sendMessage: vi.fn(),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
renderHook(() => useWindowTitle())
|
|
188
|
+
|
|
189
|
+
await waitFor(() => {
|
|
190
|
+
expect(document.title).toBe('')
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should log non-abort fetch errors', async () => {
|
|
195
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
196
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('Connection failed'))
|
|
197
|
+
vi.mocked(useWindowConnection).mockReturnValue({
|
|
198
|
+
fetch: mockFetch,
|
|
199
|
+
sendMessage: vi.fn(),
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
renderHook(() => useWindowTitle('Movies'))
|
|
203
|
+
|
|
204
|
+
await waitFor(() => {
|
|
205
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
206
|
+
'Failed to fetch app title from dashboard context:',
|
|
207
|
+
expect.any(Error),
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
consoleSpy.mockRestore()
|
|
212
|
+
})
|
|
213
|
+
})
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
const title = resolveAppTitle(data.context.resource)
|
|
85
|
+
if (title) {
|
|
86
|
+
setAppTitle(title)
|
|
87
|
+
}
|
|
88
|
+
} catch (err: unknown) {
|
|
89
|
+
if (err instanceof Error && err.name === 'AbortError') return
|
|
90
|
+
// eslint-disable-next-line no-console
|
|
91
|
+
console.error('Failed to fetch app title from dashboard context:', err)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fetchAppTitle(controller.signal)
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
controller.abort()
|
|
99
|
+
}
|
|
100
|
+
}, [fetch])
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!appTitle) return
|
|
104
|
+
|
|
105
|
+
const previous = document.title
|
|
106
|
+
document.title = viewTitle ? `${viewTitle} | ${appTitle}` : appTitle
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
document.title = previous
|
|
110
|
+
}
|
|
111
|
+
}, [viewTitle, appTitle])
|
|
112
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {applyDocumentActions, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {describe, it} from 'vitest'
|
|
3
3
|
|
|
4
|
+
import {renderHook} from '../../../test/test-utils'
|
|
4
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
6
|
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
6
7
|
|
|
@@ -31,8 +32,8 @@ describe('useApplyDocumentActions', () => {
|
|
|
31
32
|
})
|
|
32
33
|
|
|
33
34
|
it('uses the SanityInstance', async () => {
|
|
34
|
-
const
|
|
35
|
-
|
|
35
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
36
|
+
result.current({
|
|
36
37
|
type: 'document.edit',
|
|
37
38
|
documentType: 'post',
|
|
38
39
|
documentId: 'abc',
|
|
@@ -50,8 +51,8 @@ describe('useApplyDocumentActions', () => {
|
|
|
50
51
|
})
|
|
51
52
|
|
|
52
53
|
it('uses SanityInstance.match when projectId is overrideen', async () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
54
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
55
|
+
result.current({
|
|
55
56
|
type: 'document.edit',
|
|
56
57
|
documentType: 'post',
|
|
57
58
|
documentId: 'abc',
|
|
@@ -73,8 +74,8 @@ describe('useApplyDocumentActions', () => {
|
|
|
73
74
|
})
|
|
74
75
|
|
|
75
76
|
it('uses SanityInstance when dataset is overrideen', async () => {
|
|
76
|
-
const
|
|
77
|
-
|
|
77
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
78
|
+
result.current({
|
|
78
79
|
type: 'document.edit',
|
|
79
80
|
documentType: 'post',
|
|
80
81
|
documentId: 'abc',
|
|
@@ -96,8 +97,8 @@ describe('useApplyDocumentActions', () => {
|
|
|
96
97
|
})
|
|
97
98
|
|
|
98
99
|
it('uses SanityInstance.amcth when projectId and dataset is overrideen', async () => {
|
|
99
|
-
const
|
|
100
|
-
|
|
100
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
101
|
+
result.current({
|
|
101
102
|
type: 'document.edit',
|
|
102
103
|
documentType: 'post',
|
|
103
104
|
documentId: 'abc',
|
|
@@ -121,9 +122,9 @@ describe('useApplyDocumentActions', () => {
|
|
|
121
122
|
})
|
|
122
123
|
|
|
123
124
|
it("throws if SanityInstance.match doesn't find anything", async () => {
|
|
124
|
-
const
|
|
125
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
125
126
|
expect(() => {
|
|
126
|
-
|
|
127
|
+
result.current({
|
|
127
128
|
type: 'document.edit',
|
|
128
129
|
documentType: 'post',
|
|
129
130
|
documentId: 'abc',
|
|
@@ -132,4 +133,106 @@ describe('useApplyDocumentActions', () => {
|
|
|
132
133
|
})
|
|
133
134
|
}).toThrow()
|
|
134
135
|
})
|
|
136
|
+
|
|
137
|
+
it('throws when actions have mismatched project IDs', async () => {
|
|
138
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
139
|
+
expect(() => {
|
|
140
|
+
result.current([
|
|
141
|
+
{
|
|
142
|
+
type: 'document.edit',
|
|
143
|
+
documentType: 'post',
|
|
144
|
+
documentId: 'abc',
|
|
145
|
+
projectId: 'p123',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'document.edit',
|
|
149
|
+
documentType: 'post',
|
|
150
|
+
documentId: 'def',
|
|
151
|
+
projectId: 'p456',
|
|
152
|
+
},
|
|
153
|
+
])
|
|
154
|
+
}).toThrow(/Mismatched project IDs found in actions/)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('throws when actions have mismatched datasets', async () => {
|
|
158
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
159
|
+
expect(() => {
|
|
160
|
+
result.current([
|
|
161
|
+
{
|
|
162
|
+
type: 'document.edit',
|
|
163
|
+
documentType: 'post',
|
|
164
|
+
documentId: 'abc',
|
|
165
|
+
projectId: 'p',
|
|
166
|
+
dataset: 'd1',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: 'document.edit',
|
|
170
|
+
documentType: 'post',
|
|
171
|
+
documentId: 'def',
|
|
172
|
+
projectId: 'p',
|
|
173
|
+
dataset: 'd2',
|
|
174
|
+
},
|
|
175
|
+
])
|
|
176
|
+
}).toThrow(/Mismatched datasets found in actions/)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('throws when actions have mismatched sources', async () => {
|
|
180
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
181
|
+
expect(() => {
|
|
182
|
+
result.current([
|
|
183
|
+
{
|
|
184
|
+
type: 'document.edit',
|
|
185
|
+
documentType: 'post',
|
|
186
|
+
documentId: 'abc',
|
|
187
|
+
source: {projectId: 'p', dataset: 'd1'},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'document.edit',
|
|
191
|
+
documentType: 'post',
|
|
192
|
+
documentId: 'def',
|
|
193
|
+
source: {projectId: 'p', dataset: 'd2'},
|
|
194
|
+
},
|
|
195
|
+
])
|
|
196
|
+
}).toThrow(/Mismatched sources found in actions/)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('throws when mixing projectId and source (projectId first)', async () => {
|
|
200
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
201
|
+
expect(() => {
|
|
202
|
+
result.current([
|
|
203
|
+
{
|
|
204
|
+
type: 'document.edit',
|
|
205
|
+
documentType: 'post',
|
|
206
|
+
documentId: 'abc',
|
|
207
|
+
projectId: 'p',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: 'document.edit',
|
|
211
|
+
documentType: 'post',
|
|
212
|
+
documentId: 'def',
|
|
213
|
+
source: {projectId: 'p', dataset: 'd'},
|
|
214
|
+
},
|
|
215
|
+
])
|
|
216
|
+
}).toThrow(/Mismatches between projectId\/dataset options and source/)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('throws when mixing source and projectId (source first)', async () => {
|
|
220
|
+
const {result} = renderHook(() => useApplyDocumentActions())
|
|
221
|
+
expect(() => {
|
|
222
|
+
result.current([
|
|
223
|
+
{
|
|
224
|
+
type: 'document.edit',
|
|
225
|
+
documentType: 'post',
|
|
226
|
+
documentId: 'abc',
|
|
227
|
+
source: {projectId: 'p', dataset: 'd'},
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
type: 'document.edit',
|
|
231
|
+
documentType: 'post',
|
|
232
|
+
documentId: 'def',
|
|
233
|
+
projectId: 'p',
|
|
234
|
+
},
|
|
235
|
+
])
|
|
236
|
+
}).toThrow(/Mismatches between projectId\/dataset options and source/)
|
|
237
|
+
})
|
|
135
238
|
})
|