@sanity/sdk-react 3.4.0-rc.0 → 3.4.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/_exports/dashboard.d.ts +112 -34
- package/dist/_exports/dashboard.d.ts.map +1 -1
- package/dist/_exports/dashboard.js +219 -114
- package/dist/_exports/dashboard.js.map +1 -1
- package/dist/index.d.ts +21 -141
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -147
- package/dist/index.js.map +1 -1
- package/dist/{useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js → useStudioWorkspacesByProjectIdDataset-Bjwi4cLk.js} +60 -3
- package/dist/useStudioWorkspacesByProjectIdDataset-Bjwi4cLk.js.map +1 -0
- package/package.json +9 -9
- package/src/_exports/dashboard.test-d.ts +3 -0
- package/src/_exports/dashboard.ts +11 -2
- package/src/_exports/sdk-react.ts +0 -3
- package/src/components/auth/AuthBoundary.test.tsx +2 -81
- package/src/components/auth/AuthBoundary.tsx +3 -17
- package/src/components/auth/LoginCallback.test.tsx +7 -46
- package/src/components/auth/LoginCallback.tsx +4 -22
- package/src/hooks/dashboard/useAgentResourceContext.test.tsx +67 -2
- package/src/hooks/dashboard/useAgentResourceContext.ts +35 -6
- package/src/hooks/dashboard/useApplicationContext.test.tsx +95 -0
- package/src/hooks/dashboard/useApplicationContext.ts +47 -0
- package/src/hooks/dashboard/useCapabilities.test.tsx +84 -0
- package/src/hooks/dashboard/useCapabilities.ts +27 -0
- package/src/hooks/dashboard/useNavigate.test.ts +392 -5
- package/src/hooks/dashboard/useNavigate.ts +169 -26
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +116 -9
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +110 -39
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +99 -1
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +73 -2
- package/dist/useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js.map +0 -1
- package/src/hooks/auth/useHandleOAuthCallback.test.tsx +0 -16
- package/src/hooks/auth/useHandleOAuthCallback.tsx +0 -49
- package/src/hooks/auth/useOAuthAuthorize.test.tsx +0 -16
- package/src/hooks/auth/useOAuthAuthorize.tsx +0 -28
- package/src/hooks/auth/useOAuthTokens.test.tsx +0 -240
- package/src/hooks/auth/useOAuthTokens.tsx +0 -95
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {type DocumentHandle} from '@sanity/sdk'
|
|
2
|
+
import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
|
|
3
|
+
import {type MessageBusHost} from '@sanity/sdk/dashboard'
|
|
2
4
|
import {renderHook} from '@testing-library/react'
|
|
3
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
6
|
|
|
7
|
+
import {renderHook as renderHookWithInstance} from '../../../test/test-utils'
|
|
5
8
|
import {useNavigateToStudioDocument} from './useNavigateToStudioDocument'
|
|
6
9
|
|
|
7
10
|
// Mock dependencies
|
|
@@ -54,14 +57,6 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
54
57
|
}
|
|
55
58
|
})
|
|
56
59
|
|
|
57
|
-
it('returns a function and connection status', () => {
|
|
58
|
-
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
59
|
-
|
|
60
|
-
expect(result.current).toEqual({
|
|
61
|
-
navigateToStudioDocument: expect.any(Function),
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
60
|
it('sends correct navigation message when called', () => {
|
|
66
61
|
const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
67
62
|
|
|
@@ -205,3 +200,115 @@ describe('useNavigateToStudioDocument', () => {
|
|
|
205
200
|
consoleSpy.mockRestore()
|
|
206
201
|
})
|
|
207
202
|
})
|
|
203
|
+
|
|
204
|
+
describe('useNavigateToStudioDocument (message bus)', () => {
|
|
205
|
+
const MESSAGE_BUS_KEY = Symbol.for('sanity.os.bus')
|
|
206
|
+
let host: MessageBusHost
|
|
207
|
+
let updates: {url: string}[]
|
|
208
|
+
let reply: {ok: true} | {ok: false; reason: 'not-navigable' | 'interrupted' | 'failed'}
|
|
209
|
+
|
|
210
|
+
const mockDocumentHandle: DocumentHandle = {
|
|
211
|
+
documentId: 'doc123',
|
|
212
|
+
documentType: 'article',
|
|
213
|
+
projectId: 'project1',
|
|
214
|
+
dataset: 'dataset1',
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const mockWorkspace = {
|
|
218
|
+
id: 'workspace123',
|
|
219
|
+
name: 'production',
|
|
220
|
+
title: 'Production',
|
|
221
|
+
basePath: '/production',
|
|
222
|
+
projectId: 'project1',
|
|
223
|
+
dataset: 'dataset1',
|
|
224
|
+
type: 'studio',
|
|
225
|
+
userApplicationId: 'studio-app',
|
|
226
|
+
url: 'https://test.sanity.studio',
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
beforeEach(() => {
|
|
230
|
+
// Reset shared mocks so `mockSendMessage` assertions don't depend on test order.
|
|
231
|
+
vi.resetAllMocks()
|
|
232
|
+
updates = []
|
|
233
|
+
reply = {ok: true}
|
|
234
|
+
mockWorkspacesByProjectIdAndDataset = {
|
|
235
|
+
'project1:dataset1': [mockWorkspace],
|
|
236
|
+
}
|
|
237
|
+
vi.stubGlobal('__SANITY_APP_ID__', 'app')
|
|
238
|
+
host = installMessageBus({appId: 'dashboard'})
|
|
239
|
+
host.subscribe('navigation.location.update', (message) => {
|
|
240
|
+
updates.push(message.payload)
|
|
241
|
+
message.reply(reply)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
afterEach(() => {
|
|
246
|
+
resetMessageBus()
|
|
247
|
+
delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
|
|
248
|
+
vi.unstubAllGlobals()
|
|
249
|
+
vi.restoreAllMocks()
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('emits navigation.location.update with the studio edit intent url', () => {
|
|
253
|
+
const {result} = renderHookWithInstance(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
254
|
+
|
|
255
|
+
result.current.navigateToStudioDocument()
|
|
256
|
+
|
|
257
|
+
expect(mockSendMessage).not.toHaveBeenCalled()
|
|
258
|
+
expect(updates).toEqual([
|
|
259
|
+
{url: '/studios/studio-app/production/intent/edit/id=doc123;type=article/'},
|
|
260
|
+
])
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('selects the workspace matching the preferred studio url', () => {
|
|
264
|
+
const preferredUrl = 'https://preferred.sanity.studio'
|
|
265
|
+
const mockWorkspace2 = {
|
|
266
|
+
...mockWorkspace,
|
|
267
|
+
id: 'workspace2',
|
|
268
|
+
name: 'staging',
|
|
269
|
+
userApplicationId: 'preferred-app',
|
|
270
|
+
url: preferredUrl,
|
|
271
|
+
}
|
|
272
|
+
mockWorkspacesByProjectIdAndDataset = {
|
|
273
|
+
'project1:dataset1': [mockWorkspace, mockWorkspace2],
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const {result} = renderHookWithInstance(() =>
|
|
277
|
+
useNavigateToStudioDocument(mockDocumentHandle, preferredUrl),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
result.current.navigateToStudioDocument()
|
|
281
|
+
|
|
282
|
+
expect(updates).toEqual([
|
|
283
|
+
{url: '/studios/preferred-app/staging/intent/edit/id=doc123;type=article/'},
|
|
284
|
+
])
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('warns and does not emit when no workspace is found', () => {
|
|
288
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
289
|
+
mockWorkspacesByProjectIdAndDataset = {}
|
|
290
|
+
|
|
291
|
+
const {result} = renderHookWithInstance(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
292
|
+
result.current.navigateToStudioDocument()
|
|
293
|
+
|
|
294
|
+
expect(updates).toEqual([])
|
|
295
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
296
|
+
consoleSpy.mockRestore()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('warns when the host rejects the navigation', async () => {
|
|
300
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
301
|
+
reply = {ok: false, reason: 'not-navigable'}
|
|
302
|
+
|
|
303
|
+
const {result} = renderHookWithInstance(() => useNavigateToStudioDocument(mockDocumentHandle))
|
|
304
|
+
result.current.navigateToStudioDocument()
|
|
305
|
+
|
|
306
|
+
await vi.waitFor(() =>
|
|
307
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
308
|
+
'Failed to navigate to studio document',
|
|
309
|
+
expect.objectContaining({ok: false, reason: 'not-navigable'}),
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
consoleSpy.mockRestore()
|
|
313
|
+
})
|
|
314
|
+
})
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
/* eslint-disable react-compiler/react-compiler -- the transport branch in `useNavigateToStudioDocument` is a deliberate rules-of-hooks exception; the compiler refuses files that disable it */
|
|
1
2
|
import {type Bridge, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
2
3
|
import {type DocumentHandle} from '@sanity/sdk'
|
|
4
|
+
import {isDashboardEnvironment} from '@sanity/sdk/_internal'
|
|
3
5
|
import {useCallback} from 'react'
|
|
4
6
|
|
|
7
|
+
import {urlFor} from '../../dashboard/urlFor'
|
|
5
8
|
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
9
|
+
import {useEmit} from './useEmit'
|
|
6
10
|
import {
|
|
7
11
|
type DashboardResource,
|
|
8
12
|
useStudioWorkspacesByProjectIdDataset,
|
|
@@ -16,6 +20,61 @@ export interface NavigateToStudioResult {
|
|
|
16
20
|
navigateToStudioDocument: () => void
|
|
17
21
|
}
|
|
18
22
|
|
|
23
|
+
function useComlinkStudioNavigator(
|
|
24
|
+
documentHandle: DocumentHandle,
|
|
25
|
+
): (workspace: DashboardResource) => void {
|
|
26
|
+
const {sendMessage} = useWindowConnection<Bridge.Navigation.NavigateToResourceMessage, never>({
|
|
27
|
+
name: SDK_NODE_NAME,
|
|
28
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
return useCallback(
|
|
32
|
+
(workspace: DashboardResource) => {
|
|
33
|
+
const message: Bridge.Navigation.NavigateToResourceMessage = {
|
|
34
|
+
type: 'dashboard/v1/bridge/navigate-to-resource',
|
|
35
|
+
data: {
|
|
36
|
+
resourceId: workspace.id,
|
|
37
|
+
resourceType: 'studio',
|
|
38
|
+
path: `/intent/edit/id=${documentHandle.documentId};type=${documentHandle.documentType}`,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
sendMessage(message.type, message.data)
|
|
42
|
+
},
|
|
43
|
+
[documentHandle, sendMessage],
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function useBusStudioNavigator(
|
|
48
|
+
documentHandle: DocumentHandle,
|
|
49
|
+
): (workspace: DashboardResource) => void {
|
|
50
|
+
const navigate = useEmit('navigation.location.update')
|
|
51
|
+
|
|
52
|
+
return useCallback(
|
|
53
|
+
(workspace: DashboardResource) => {
|
|
54
|
+
const url = urlFor
|
|
55
|
+
.studios(workspace.userApplicationId)
|
|
56
|
+
.workspace(workspace.name)
|
|
57
|
+
.intent('edit', {id: documentHandle.documentId, type: documentHandle.documentType})
|
|
58
|
+
.url()
|
|
59
|
+
navigate({url}).then(
|
|
60
|
+
(reply) => {
|
|
61
|
+
if (reply.ok === false) {
|
|
62
|
+
// eslint-disable-next-line no-console
|
|
63
|
+
console.warn('Failed to navigate to studio document', reply)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
// The reply can reject (no responder, timeout, aborted); navigation is best-effort, so
|
|
67
|
+
// warn instead of throwing.
|
|
68
|
+
(error) => {
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.warn('Failed to navigate to studio document', error)
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
[documentHandle, navigate],
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
19
78
|
/**
|
|
20
79
|
* @public
|
|
21
80
|
*
|
|
@@ -24,6 +83,9 @@ export interface NavigateToStudioResult {
|
|
|
24
83
|
* Uses the `projectId` and `dataset` properties of the {@link DocumentHandle} you provide to resolve the correct Studio.
|
|
25
84
|
* This will only work if you have deployed a studio with a workspace with this `projectId` / `dataset` combination.
|
|
26
85
|
*
|
|
86
|
+
* Works in both Dashboard runtimes: it sends the navigation over the Comlink connection in the
|
|
87
|
+
* iframe runtime and over the message bus in the federated runtime.
|
|
88
|
+
*
|
|
27
89
|
* @remarks If you write your own Document Handle to pass to this hook (as opposed to a Document Handle generated by another hook),
|
|
28
90
|
* it must include values for `documentId`, `documentType`, `projectId`, and `dataset`.
|
|
29
91
|
*
|
|
@@ -66,10 +128,13 @@ export function useNavigateToStudioDocument(
|
|
|
66
128
|
preferredStudioUrl?: string,
|
|
67
129
|
): NavigateToStudioResult {
|
|
68
130
|
const {workspacesByProjectIdAndDataset} = useStudioWorkspacesByProjectIdDataset()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
131
|
+
// The branch is stable: the transport is fixed for the page lifetime, so one navigator hook
|
|
132
|
+
// always runs and the other never does.
|
|
133
|
+
const sendToWorkspace = isDashboardEnvironment()
|
|
134
|
+
? // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime
|
|
135
|
+
useBusStudioNavigator(documentHandle)
|
|
136
|
+
: // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime
|
|
137
|
+
useComlinkStudioNavigator(documentHandle)
|
|
73
138
|
|
|
74
139
|
const navigateToStudioDocument = useCallback(() => {
|
|
75
140
|
const {projectId, dataset} = documentHandle
|
|
@@ -80,30 +145,11 @@ export function useNavigateToStudioDocument(
|
|
|
80
145
|
return
|
|
81
146
|
}
|
|
82
147
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const allWorkspaces = [
|
|
89
|
-
...(workspacesByProjectIdAndDataset[`${projectId}:${dataset}`] || []),
|
|
90
|
-
...(workspacesByProjectIdAndDataset['NO_PROJECT_ID:NO_DATASET'] || []),
|
|
91
|
-
]
|
|
92
|
-
workspace = allWorkspaces.find((w) => w.url === preferredStudioUrl)
|
|
93
|
-
} else {
|
|
94
|
-
const workspaces = workspacesByProjectIdAndDataset[`${projectId}:${dataset}`]
|
|
95
|
-
if (workspaces?.length > 1) {
|
|
96
|
-
// eslint-disable-next-line no-console
|
|
97
|
-
console.warn(
|
|
98
|
-
'Multiple workspaces found for document and no preferred studio url',
|
|
99
|
-
documentHandle,
|
|
100
|
-
)
|
|
101
|
-
// eslint-disable-next-line no-console
|
|
102
|
-
console.warn('Using the first one', workspaces[0])
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
workspace = workspaces?.[0]
|
|
106
|
-
}
|
|
148
|
+
const workspace = resolveWorkspace(
|
|
149
|
+
documentHandle,
|
|
150
|
+
workspacesByProjectIdAndDataset,
|
|
151
|
+
preferredStudioUrl,
|
|
152
|
+
)
|
|
107
153
|
|
|
108
154
|
if (!workspace) {
|
|
109
155
|
// eslint-disable-next-line no-console
|
|
@@ -113,19 +159,44 @@ export function useNavigateToStudioDocument(
|
|
|
113
159
|
return
|
|
114
160
|
}
|
|
115
161
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
data: {
|
|
119
|
-
resourceId: workspace.id,
|
|
120
|
-
resourceType: 'studio',
|
|
121
|
-
path: `/intent/edit/id=${documentHandle.documentId};type=${documentHandle.documentType}`,
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
sendMessage(message.type, message.data)
|
|
126
|
-
}, [documentHandle, workspacesByProjectIdAndDataset, sendMessage, preferredStudioUrl])
|
|
162
|
+
sendToWorkspace(workspace)
|
|
163
|
+
}, [documentHandle, workspacesByProjectIdAndDataset, preferredStudioUrl, sendToWorkspace])
|
|
127
164
|
|
|
128
165
|
return {
|
|
129
166
|
navigateToStudioDocument,
|
|
130
167
|
}
|
|
131
168
|
}
|
|
169
|
+
|
|
170
|
+
type WorkspacesByProjectIdDataset = ReturnType<
|
|
171
|
+
typeof useStudioWorkspacesByProjectIdDataset
|
|
172
|
+
>['workspacesByProjectIdAndDataset']
|
|
173
|
+
|
|
174
|
+
function resolveWorkspace(
|
|
175
|
+
documentHandle: DocumentHandle,
|
|
176
|
+
workspacesByProjectIdAndDataset: WorkspacesByProjectIdDataset,
|
|
177
|
+
preferredStudioUrl?: string,
|
|
178
|
+
): DashboardResource | undefined {
|
|
179
|
+
const {projectId, dataset} = documentHandle
|
|
180
|
+
const key = `${projectId}:${dataset}` as const
|
|
181
|
+
|
|
182
|
+
if (preferredStudioUrl) {
|
|
183
|
+
// Include workspaces without projectId/dataset in case their manifest has not loaded yet.
|
|
184
|
+
const allWorkspaces = [
|
|
185
|
+
...(workspacesByProjectIdAndDataset[key] || []),
|
|
186
|
+
...(workspacesByProjectIdAndDataset['NO_PROJECT_ID:NO_DATASET'] || []),
|
|
187
|
+
]
|
|
188
|
+
return allWorkspaces.find((w) => w.url === preferredStudioUrl)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const workspaces = workspacesByProjectIdAndDataset[key]
|
|
192
|
+
if (workspaces && workspaces.length > 1) {
|
|
193
|
+
// eslint-disable-next-line no-console
|
|
194
|
+
console.warn(
|
|
195
|
+
'Multiple workspaces found for document and no preferred studio url',
|
|
196
|
+
documentHandle,
|
|
197
|
+
)
|
|
198
|
+
// eslint-disable-next-line no-console
|
|
199
|
+
console.warn('Using the first one', workspaces[0])
|
|
200
|
+
}
|
|
201
|
+
return workspaces?.[0]
|
|
202
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
|
|
2
|
+
import {type ApplicationActivity, type MessageBusHost} from '@sanity/sdk/dashboard'
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
4
|
|
|
3
5
|
import {renderHook} from '../../../test/test-utils'
|
|
4
6
|
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
@@ -69,3 +71,99 @@ describe('useRecordDocumentHistoryEvent', () => {
|
|
|
69
71
|
)
|
|
70
72
|
})
|
|
71
73
|
})
|
|
74
|
+
|
|
75
|
+
describe('useRecordDocumentHistoryEvent (message bus)', () => {
|
|
76
|
+
const MESSAGE_BUS_KEY = Symbol.for('sanity.os.bus')
|
|
77
|
+
let host: MessageBusHost
|
|
78
|
+
|
|
79
|
+
const documentHandle = {
|
|
80
|
+
documentId: 'mock-id',
|
|
81
|
+
documentType: 'mock-type',
|
|
82
|
+
resourceType: 'studio' as const,
|
|
83
|
+
resourceId: 'mock-resource-id',
|
|
84
|
+
schemaName: 'production',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const emitCapabilities = (value: {history?: boolean}) =>
|
|
88
|
+
host.connections.subscribe((client) => client.emit('applications.capabilities', value))
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.stubGlobal('__SANITY_APP_ID__', 'app')
|
|
92
|
+
host = installMessageBus({appId: 'dashboard'})
|
|
93
|
+
emitCapabilities({history: true})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
resetMessageBus()
|
|
98
|
+
delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
|
|
99
|
+
vi.unstubAllGlobals()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it.each(['viewed', 'edited', 'created', 'deleted'] as const)(
|
|
103
|
+
'emits a %s document activity event, mapping studios to their dataset resource',
|
|
104
|
+
(eventType) => {
|
|
105
|
+
const activity: ApplicationActivity[] = []
|
|
106
|
+
host.subscribe('applications.activity', (message) => activity.push(message.payload))
|
|
107
|
+
|
|
108
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(documentHandle))
|
|
109
|
+
result.current.recordEvent(eventType)
|
|
110
|
+
|
|
111
|
+
expect(useWindowConnection).not.toHaveBeenCalled()
|
|
112
|
+
expect(activity).toEqual([
|
|
113
|
+
{
|
|
114
|
+
kind: 'document',
|
|
115
|
+
eventType,
|
|
116
|
+
document: {
|
|
117
|
+
id: 'mock-id',
|
|
118
|
+
type: 'mock-type',
|
|
119
|
+
resource: {id: 'mock-resource-id', type: 'dataset', schemaName: 'production'},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
])
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
it('passes a non-studio resource type through unmapped', () => {
|
|
127
|
+
const activity: ApplicationActivity[] = []
|
|
128
|
+
host.subscribe('applications.activity', (message) => activity.push(message.payload))
|
|
129
|
+
|
|
130
|
+
const {result} = renderHook(() =>
|
|
131
|
+
useRecordDocumentHistoryEvent({...documentHandle, resourceType: 'media-library'}),
|
|
132
|
+
)
|
|
133
|
+
result.current.recordEvent('viewed')
|
|
134
|
+
|
|
135
|
+
expect(activity[0].document.resource.type).toBe('media-library')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it.each([{history: false}, {}])(
|
|
139
|
+
'does not emit when the host does not provide the history capability (%o)',
|
|
140
|
+
(capabilities) => {
|
|
141
|
+
emitCapabilities(capabilities)
|
|
142
|
+
const activity: ApplicationActivity[] = []
|
|
143
|
+
host.subscribe('applications.activity', (message) => activity.push(message.payload))
|
|
144
|
+
|
|
145
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(documentHandle))
|
|
146
|
+
result.current.recordEvent('viewed')
|
|
147
|
+
|
|
148
|
+
expect(activity).toEqual([])
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
it('throws when resourceId is missing', () => {
|
|
153
|
+
expect(() =>
|
|
154
|
+
renderHook(() => useRecordDocumentHistoryEvent({...documentHandle, resourceId: undefined})),
|
|
155
|
+
).toThrow('resourceId is required to record document history under the message bus')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('keeps recordEvent referentially stable when capabilities re-publish unchanged', () => {
|
|
159
|
+
const {result, rerender} = renderHook(() => useRecordDocumentHistoryEvent(documentHandle))
|
|
160
|
+
const first = result.current.recordEvent
|
|
161
|
+
|
|
162
|
+
// Re-publishing the same value must not churn the callback: a dependency on the whole
|
|
163
|
+
// capabilities object rather than `capabilities.history` would fail here.
|
|
164
|
+
emitCapabilities({history: true})
|
|
165
|
+
rerender()
|
|
166
|
+
|
|
167
|
+
expect(result.current.recordEvent).toBe(first)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable react-compiler/react-compiler -- the transport branch in `useRecordDocumentHistoryEvent` is a deliberate rules-of-hooks exception; the compiler refuses files that disable it */
|
|
1
2
|
import {
|
|
2
3
|
type CanvasResource,
|
|
3
4
|
type Events,
|
|
@@ -7,10 +8,13 @@ import {
|
|
|
7
8
|
type StudioResource,
|
|
8
9
|
} from '@sanity/message-protocol'
|
|
9
10
|
import {type DocumentHandle} from '@sanity/sdk'
|
|
11
|
+
import {isDashboardEnvironment} from '@sanity/sdk/_internal'
|
|
10
12
|
import {type FrameMessage} from '@sanity/sdk/comlink'
|
|
11
13
|
import {useCallback} from 'react'
|
|
12
14
|
|
|
13
15
|
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
16
|
+
import {useEmit} from './useEmit'
|
|
17
|
+
import {useTopic} from './useTopic'
|
|
14
18
|
|
|
15
19
|
interface DocumentInteractionHistory {
|
|
16
20
|
recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
|
|
@@ -21,6 +25,11 @@ interface DocumentInteractionHistory {
|
|
|
21
25
|
*/
|
|
22
26
|
interface UseRecordDocumentHistoryEventProps extends DocumentHandle {
|
|
23
27
|
resourceType: StudioResource['type'] | MediaResource['type'] | CanvasResource['type']
|
|
28
|
+
/**
|
|
29
|
+
* The resource the document lives in. Optional for studios over Comlink, where the iframe
|
|
30
|
+
* host fills it from context; always required under the message bus, which has no such
|
|
31
|
+
* context and throws when it is missing.
|
|
32
|
+
*/
|
|
24
33
|
resourceId?: string
|
|
25
34
|
/**
|
|
26
35
|
* The name of the schema collection this document belongs to.
|
|
@@ -31,8 +40,20 @@ interface UseRecordDocumentHistoryEventProps extends DocumentHandle {
|
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
42
|
* @internal
|
|
34
|
-
* Hook for
|
|
43
|
+
* Hook for recording document interaction history in a Dashboard application.
|
|
35
44
|
* This hook provides functionality to record document interactions.
|
|
45
|
+
*
|
|
46
|
+
* It works in both Dashboard runtimes and picks the transport for the current one:
|
|
47
|
+
*
|
|
48
|
+
* | Runtime | Transport | `resourceId` | Suspends until |
|
|
49
|
+
* | --- | --- | --- | --- |
|
|
50
|
+
* | iframe | Comlink | optional for studios | the node connects |
|
|
51
|
+
* | federated | message bus | required | capabilities publish |
|
|
52
|
+
*
|
|
53
|
+
* Under the message bus the hook always suspends until the host publishes its capabilities;
|
|
54
|
+
* only once they resolve does `recordEvent` run, and it then no-ops on every call when the
|
|
55
|
+
* host does not provide the `history` capability. There is no synchronous no-op path.
|
|
56
|
+
*
|
|
36
57
|
* @category History
|
|
37
58
|
* @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
|
|
38
59
|
* @returns An object containing:
|
|
@@ -70,7 +91,18 @@ interface UseRecordDocumentHistoryEventProps extends DocumentHandle {
|
|
|
70
91
|
* }
|
|
71
92
|
* ```
|
|
72
93
|
*/
|
|
73
|
-
export function useRecordDocumentHistoryEvent(
|
|
94
|
+
export function useRecordDocumentHistoryEvent(
|
|
95
|
+
props: UseRecordDocumentHistoryEventProps,
|
|
96
|
+
): DocumentInteractionHistory {
|
|
97
|
+
// The branch is stable: the transport is fixed for the page lifetime, so one set of hooks
|
|
98
|
+
// always runs and the other never does.
|
|
99
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime
|
|
100
|
+
if (isDashboardEnvironment()) return useBusRecordDocumentHistoryEvent(props)
|
|
101
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime
|
|
102
|
+
return useComlinkRecordDocumentHistoryEvent(props)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function useComlinkRecordDocumentHistoryEvent({
|
|
74
106
|
documentId,
|
|
75
107
|
documentType,
|
|
76
108
|
resourceType,
|
|
@@ -119,3 +151,42 @@ export function useRecordDocumentHistoryEvent({
|
|
|
119
151
|
recordEvent,
|
|
120
152
|
}
|
|
121
153
|
}
|
|
154
|
+
|
|
155
|
+
function useBusRecordDocumentHistoryEvent({
|
|
156
|
+
documentId,
|
|
157
|
+
documentType,
|
|
158
|
+
resourceType,
|
|
159
|
+
resourceId,
|
|
160
|
+
schemaName,
|
|
161
|
+
}: UseRecordDocumentHistoryEventProps): DocumentInteractionHistory {
|
|
162
|
+
const emitActivity = useEmit('applications.activity')
|
|
163
|
+
const capabilities = useTopic('applications.capabilities')
|
|
164
|
+
|
|
165
|
+
// The bus host has no iframe context to fill `projectId.dataset` from, so the resource must
|
|
166
|
+
// be addressed explicitly.
|
|
167
|
+
if (!resourceId) {
|
|
168
|
+
throw new Error('resourceId is required to record document history under the message bus')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// A studio is addressed by its dataset resource over the bus; the workspace name rides along
|
|
172
|
+
// in `schemaName`.
|
|
173
|
+
const type = resourceType === 'studio' ? 'dataset' : resourceType
|
|
174
|
+
|
|
175
|
+
const recordEvent = useCallback(
|
|
176
|
+
(eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
|
|
177
|
+
if (!capabilities.history) return
|
|
178
|
+
emitActivity({
|
|
179
|
+
kind: 'document',
|
|
180
|
+
eventType,
|
|
181
|
+
document: {
|
|
182
|
+
id: documentId,
|
|
183
|
+
type: documentType,
|
|
184
|
+
resource: {id: resourceId, type, schemaName},
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
},
|
|
188
|
+
[capabilities.history, documentId, documentType, emitActivity, resourceId, schemaName, type],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return {recordEvent}
|
|
192
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js","names":["SanityInstance","createContext","SanityInstanceContext","SanityInstance","useContext","SanityInstanceContext","useSanityInstance","instance","Error","SanityConfig","SanityInstance","StateSource","useSyncExternalStore","useSanityInstance","StateSourceFactory","instance","params","TParams","TState","CreateStateSourceHookOptions","getState","shouldSuspend","suspender","Promise","getConfig","createStateSourceHook","options","suspense","undefined","useHook","t0","$","_c","t1","state","subscribe","getCurrent","AuthState","getAuthState","createStateSourceHook","useAuthState","MessageData","NodeInput","SanityInstance","StateSource","FrameMessage","getNodeState","NodeState","WindowMessage","useCallback","useEffect","useRef","filter","firstValueFrom","useSanityInstance","createStateSourceHook","WindowMessageHandler","event","TFrameMessage","UseWindowConnectionOptions","name","connectTo","onMessage","Record","TMessage","WindowConnection","sendMessage","type","TType","data","Extract","fetch","options","signal","AbortSignal","suppressWarnings","responseTimeout","Promise","TResponse","useNodeState","getState","instance","nodeInput","shouldSuspend","getCurrent","undefined","suspender","observable","pipe","Boolean","useWindowConnection","t0","$","_c","t1","node","t2","Symbol","for","messageUnsubscribers","t3","Object","entries","forEach","t4","handler","messageUnsubscribe","on","current","push","_temp","t5","type_0","post","t6","type_1","data_0","fetchOptions","t7","unsubscribe","React","MODULE_SLOT_KEY","Symbol","for","ModuleContext","Context","ModuleSlot","WeakMap","createContext","getDashboardModuleContext","globals","globalThis","slot","key","context","get","undefined","set","ClientError","AuthStateType","setAuthToken","getDashboardMessageBus","MessageBus","React","PropsWithChildren","useContext","useEffect","useRef","useState","defer","of","catchError","getDashboardModuleContext","useAuthState","useSanityInstance","DashboardTokenRefresh","t0","$","_c","children","messageBus","instance","authState","processed401ErrorRef","t1","t2","subscription","subscribe","pipe","_temp","token","unsubscribe","t3","error","type","has401Error","ERROR","statusCode","current","emit","undefined","catch","_temp2","t4","console","warn","DashboardTokenRefreshProvider","FC","Symbol","for","moduleId","getDashboardOrganizationId","OrganizationBase","getTopicState","isDashboardEnvironment","useMemo","useSyncExternalStore","useSanityInstance","CurrentOrganization","Pick","useOrganizationId","$","_c","instance","t0","bb0","t1","source","t2","getCurrent","id","undefined","t3","subscribe","getTopicState","resolveTopic","StateTopic","TopicData","createStateSourceHook","useTopic","getState","shouldSuspend","instance","topic","getCurrent","undefined","suspender","K","SDK_CHANNEL_NAME","SDK_NODE_NAME","getApplicationOrigin","isDashboardEnvironment","TopicData","useEffect","useMemo","useState","useWindowConnection","useTopic","DashboardResource","id","name","title","basePath","projectId","dataset","type","userApplicationId","url","WorkspacesByProjectIdDataset","key","StudioWorkspacesResult","workspacesByProjectIdAndDataset","error","DashboardApplications","NonNullable","useStudioWorkspacesByProjectIdDataset","useBusStudioWorkspaces","useComlinkStudioWorkspaces","toResources","application","activeDeployment","workspaces","map","workspace","toWorkspaceMap","applications","workspaceMap","resource","flatMap","const","push","$","_c","t0","t1","t2","Symbol","for","setWorkspacesByProjectIdAndDataset","setError","connectTo","fetch","t3","fetchWorkspaces","signal","data","undefined","noProjectIdAndDataset","context","availableResources","forEach","length","t4","err","Error","controller","AbortController","abort"],"sources":["../src/context/SanityInstanceContext.ts","../src/hooks/context/useSanityInstance.ts","../src/hooks/helpers/createStateSourceHook.tsx","../src/hooks/auth/useAuthState.tsx","../src/hooks/comlink/useWindowConnection.ts","../src/dashboard/module.ts","../src/context/DashboardTokenRefresh.tsx","../src/hooks/dashboard/useOrganizationId.tsx","../src/hooks/dashboard/useTopic.ts","../src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts"],"sourcesContent":["import {type SanityInstance} from '@sanity/sdk'\nimport {createContext} from 'react'\n\nexport const SanityInstanceContext = createContext<SanityInstance | null>(null)\n","import {type SanityInstance} from '@sanity/sdk'\nimport {useContext} from 'react'\n\nimport {SanityInstanceContext} from '../../context/SanityInstanceContext'\n\n/**\n * Retrieves the current Sanity instance from context\n *\n * @public\n *\n * @category Platform\n * @returns The current Sanity instance\n *\n * @remarks\n * This hook accesses the nearest Sanity instance from the React context.\n * The hook must be used within a component wrapped by a `ResourceProvider` or `SanityApp`.\n *\n * @example Get the current instance\n * ```tsx\n * const instance = useSanityInstance()\n * console.log(instance.config.projectId)\n * ```\n *\n * @throws Error if no SanityInstance is found in context\n */\nexport const useSanityInstance = (): SanityInstance => {\n const instance = useContext(SanityInstanceContext)\n\n if (!instance) {\n throw new Error(\n `SanityInstance context not found. Please ensure that your component is wrapped in a ResourceProvider or a SanityApp component.`,\n )\n }\n\n return instance\n}\n","import {type SanityConfig, type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\ntype StateSourceFactory<TParams extends unknown[], TState> = (\n instance: SanityInstance,\n ...params: TParams\n) => StateSource<TState>\n\ninterface CreateStateSourceHookOptions<TParams extends unknown[], TState> {\n getState: StateSourceFactory<TParams, TState>\n shouldSuspend?: (instance: SanityInstance, ...params: TParams) => boolean\n suspender?: (instance: SanityInstance, ...params: TParams) => Promise<unknown>\n getConfig?: (...params: TParams) => SanityConfig | undefined\n}\n\nexport function createStateSourceHook<TParams extends unknown[], TState>(\n options: StateSourceFactory<TParams, TState> | CreateStateSourceHookOptions<TParams, TState>,\n): (...params: TParams) => TState {\n const getState = typeof options === 'function' ? options : options.getState\n const suspense = 'shouldSuspend' in options && 'suspender' in options ? options : undefined\n\n function useHook(...params: TParams) {\n const instance = useSanityInstance()\n\n if (suspense?.suspender && suspense?.shouldSuspend?.(instance, ...params)) {\n throw suspense.suspender(instance, ...params)\n }\n\n const state = getState(instance, ...params)\n return useSyncExternalStore(state.subscribe, state.getCurrent)\n }\n\n return useHook\n}\n","import {type AuthState, getAuthState} from '@sanity/sdk'\n\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * @internal\n * A React hook that subscribes to authentication state changes.\n *\n * This hook provides access to the current authentication state type from the Sanity auth store.\n * It automatically re-renders when the authentication state changes.\n *\n * @remarks\n * The hook uses `useSyncExternalStore` to safely subscribe to auth state changes\n * and ensure consistency between server and client rendering.\n *\n * @returns The current authentication state type\n *\n * @example\n * ```tsx\n * function AuthStatus() {\n * const authState = useAuthState()\n * return <div>Current auth state: {authState}</div>\n * }\n * ```\n */\nexport const useAuthState: () => AuthState = createStateSourceHook(getAuthState)\n","import {type MessageData, type NodeInput} from '@sanity/comlink'\nimport {type SanityInstance, type StateSource} from '@sanity/sdk'\nimport {\n type FrameMessage,\n getNodeState,\n type NodeState,\n type WindowMessage,\n} from '@sanity/sdk/comlink'\nimport {useCallback, useEffect, useRef} from 'react'\nimport {filter, firstValueFrom} from 'rxjs'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * @internal\n */\nexport type WindowMessageHandler<TFrameMessage extends FrameMessage> = (\n event: TFrameMessage['data'],\n) => TFrameMessage['response']\n\n/**\n * @internal\n */\nexport interface UseWindowConnectionOptions<TMessage extends FrameMessage> {\n name: string\n connectTo: string\n onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>\n}\n\n/**\n * @internal\n */\nexport interface WindowConnection<TMessage extends WindowMessage> {\n sendMessage: <TType extends TMessage['type']>(\n type: TType,\n data?: Extract<TMessage, {type: TType}>['data'],\n ) => void\n fetch: <TResponse>(\n type: string,\n data?: MessageData,\n options?: {\n signal?: AbortSignal\n suppressWarnings?: boolean\n responseTimeout?: number\n },\n ) => Promise<TResponse>\n}\n\nconst useNodeState = createStateSourceHook({\n getState: getNodeState as (\n instance: SanityInstance,\n nodeInput: NodeInput,\n ) => StateSource<NodeState>,\n shouldSuspend: (instance: SanityInstance, nodeInput: NodeInput) =>\n getNodeState(instance, nodeInput).getCurrent() === undefined,\n suspender: (instance: SanityInstance, nodeInput: NodeInput) => {\n return firstValueFrom(getNodeState(instance, nodeInput).observable.pipe(filter(Boolean)))\n },\n})\n\n/**\n * @internal\n * Hook to wrap a Comlink node in a React hook.\n * Our store functionality takes care of the lifecycle of the node,\n * as well as sharing a single node between invocations if they share the same name.\n *\n * Generally not to be used directly, but to be used as a dependency of\n * Comlink-powered hooks like `useStudioWorkspacesByProjectIdDataset`.\n */\nexport function useWindowConnection<\n TWindowMessage extends WindowMessage,\n TFrameMessage extends FrameMessage,\n>({\n name,\n connectTo,\n onMessage,\n}: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {\n const {node} = useNodeState({name, connectTo})\n const messageUnsubscribers = useRef<(() => void)[]>([])\n const instance = useSanityInstance()\n\n useEffect(() => {\n if (onMessage) {\n Object.entries(onMessage).forEach(([type, handler]) => {\n const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)\n if (messageUnsubscribe) {\n messageUnsubscribers.current.push(messageUnsubscribe)\n }\n })\n }\n\n return () => {\n messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())\n messageUnsubscribers.current = []\n }\n }, [instance, name, onMessage, node])\n\n const sendMessage = useCallback(\n (type: TWindowMessage['type'], data?: Extract<TWindowMessage, {type: typeof type}>['data']) => {\n node.post(type, data)\n },\n [node],\n )\n\n const fetch = useCallback(\n <TResponse>(\n type: string,\n data?: MessageData,\n fetchOptions?: {\n responseTimeout?: number\n signal?: AbortSignal\n suppressWarnings?: boolean\n },\n ): Promise<TResponse> => {\n return node.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>\n },\n [node],\n )\n return {\n sendMessage,\n fetch,\n }\n}\n","import * as React from 'react'\n\nconst MODULE_SLOT_KEY = Symbol.for('sanity.os.module')\n\ntype ModuleContext = React.Context<string | undefined>\ntype ModuleSlot = WeakMap<typeof React.createContext, ModuleContext>\n\n/**\n * Returns the React context that carries the current federation module id.\n *\n * @remarks\n * This is the slot the CLI-generated wrapper populates with\n * `renderOptions.moduleId`. The context lives in a per-React-copy slot on\n * `globalThis` so the CLI wrapper and the SDK share the same context even\n * across module copies. Each React copy gets its own context, since a context\n * created by one copy is inert in another.\n *\n * The slot is keyed on `React.createContext` rather than the `React` namespace\n * object: `import * as React` and `import React from 'react'` can yield\n * different wrapper objects for the same React copy under bundler interop,\n * whereas the `createContext` function is the same reference under both.\n *\n * The provider side lives in the CLI, which must not import the SDK:\n * `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in\n * `sanity-io/cli` reconstructs this accessor (same symbol, same key) and\n * wraps `App` in the context's `Provider`. Nothing in this repo exports it.\n * @internal\n */\nexport function getDashboardModuleContext(): ModuleContext {\n const globals = globalThis as {[MODULE_SLOT_KEY]?: ModuleSlot}\n const slot = (globals[MODULE_SLOT_KEY] ??= new WeakMap())\n const key = React.createContext\n let context = slot.get(key)\n if (!context) {\n context = React.createContext<string | undefined>(undefined)\n slot.set(key, context)\n }\n return context\n}\n","import {type ClientError} from '@sanity/client'\nimport {AuthStateType, setAuthToken} from '@sanity/sdk'\nimport {getDashboardMessageBus} from '@sanity/sdk/_internal'\nimport {type MessageBus} from '@sanity/sdk/dashboard'\nimport React, {type PropsWithChildren, useContext, useEffect, useRef, useState} from 'react'\nimport {defer, of} from 'rxjs'\nimport {catchError} from 'rxjs/operators'\n\nimport {getDashboardModuleContext} from '../dashboard/module'\nimport {useAuthState} from '../hooks/auth/useAuthState'\nimport {useSanityInstance} from '../hooks/context/useSanityInstance'\n\n/**\n * Keeps the SDK auth token in sync with the dashboard \"OS\".\n *\n * When running inside the dashboard the OS owns the session, so we subscribe\n * to its `auth.token` stream and mirror each value into\n * the auth store — a token logs us in, `null` logs us out, and later OS\n * sign-in/out propagates automatically. When a request is rejected with a 401\n * (the token expired), we ask the OS to reissue rather than tearing the session\n * down; the new token arrives back through the same subscription.\n */\nfunction DashboardTokenRefresh({\n children,\n messageBus,\n}: PropsWithChildren<{messageBus: MessageBus}>) {\n const instance = useSanityInstance()\n const authState = useAuthState()\n const processed401ErrorRef = useRef<unknown | null>(null)\n\n useEffect(() => {\n const subscription = defer(() => messageBus.subscribe('auth.token'))\n .pipe(catchError(() => of(null)))\n .subscribe((token) => setAuthToken(instance, token))\n return () => subscription.unsubscribe()\n }, [instance, messageBus])\n\n useEffect(() => {\n const has401Error =\n authState.type === AuthStateType.ERROR && (authState.error as ClientError)?.statusCode === 401\n\n if (has401Error && processed401ErrorRef.current !== authState.error) {\n processed401ErrorRef.current = authState.error\n // Event topics have no replay, so a missing responder or timeout is otherwise dropped silently.\n messageBus.emit('auth.token.refresh', undefined).catch((error) => {\n // eslint-disable-next-line no-console\n console.warn('[sanity/sdk] Dashboard token refresh failed:', error)\n })\n } else if (!has401Error) {\n processed401ErrorRef.current = null\n }\n }, [authState, messageBus])\n\n return children\n}\n\n/**\n * Authenticates the SDK with the Sanity Dashboard's session when the app runs\n * inside the dashboard.\n *\n * The dashboard owns the session there: this provider subscribes to the token\n * the dashboard issues, writes each new value into the SDK's auth store (where\n * SDK hooks read it from), and asks the dashboard for a fresh token when a\n * request fails with a 401. Outside the dashboard it renders children\n * unchanged and the app's normal auth flow applies.\n *\n * @remarks\n * `AuthBoundary` mounts this automatically, so most apps never need it\n * directly. Mount it yourself only when your app runs inside the dashboard\n * without `AuthBoundary` — that is, the app renders its own loading and error\n * UI instead of the SDK's login flow — but still uses SDK hooks such as\n * `useQuery`, which need the dashboard's token in the auth store to\n * authenticate their requests.\n *\n * Mount it once, inside the provider that creates the Sanity instance whose\n * store should receive the token.\n *\n * @example\n * ```tsx\n * import {ResourceProvider} from '@sanity/sdk-react'\n * import {TokenRefreshProvider} from '@sanity/sdk-react/dashboard'\n *\n * function EmbeddedApp() {\n * return (\n * <ResourceProvider fallback={<Loading />}>\n * <TokenRefreshProvider>\n * <App />\n * </TokenRefreshProvider>\n * </ResourceProvider>\n * )\n * }\n * ```\n *\n * @public\n */\nexport const DashboardTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {\n const instance = useSanityInstance()\n const moduleId = useContext(getDashboardModuleContext())\n // The connection is first-caller-wins per instance, and hooks below read it during their\n // render, before any effect here could run. Connecting in the first render pins the module\n // identity before they do; nothing has subscribed to the store yet, so the write is safe.\n // The module id is read once: the CLI wrapper provides it statically above this tree.\n // No retry: the host installs the bus at module evaluation, before any remote renders, and\n // a standalone app has no host to wait for.\n const [messageBus] = useState(() => getDashboardMessageBus(instance, moduleId))\n if (messageBus) {\n return <DashboardTokenRefresh messageBus={messageBus}>{children}</DashboardTokenRefresh>\n }\n\n return children\n}\n","import {getDashboardOrganizationId, type OrganizationBase} from '@sanity/sdk'\nimport {getTopicState, isDashboardEnvironment} from '@sanity/sdk/_internal'\nimport {useMemo, useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\n\ntype CurrentOrganization = Pick<OrganizationBase, 'id' | 'name' | 'slug'> | null | undefined\n\n/**\n * @public\n *\n * A React hook that retrieves the dashboard organization ID that is currently selected in the Sanity Dashboard.\n *\n * Works in both Dashboard runtimes: it reads the `organizations.current` message bus topic when a\n * host has installed the bus, and falls back to the Comlink connection otherwise.\n *\n * @example\n * ```tsx\n * function DashboardComponent() {\n * const orgId = useOrganizationId()\n *\n * if (!orgId) return null\n *\n * return <div>Organization ID: {String(orgId)}</div>\n * }\n * ```\n *\n * @category Dashboard\n * @returns The dashboard organization ID (string | undefined)\n */\nexport function useOrganizationId(): string | undefined {\n const instance = useSanityInstance()\n const {subscribe, getCurrent} = useMemo(() => {\n if (!isDashboardEnvironment()) return getDashboardOrganizationId(instance)\n const source = getTopicState(instance, 'organizations.current')\n return {\n subscribe: source.subscribe,\n getCurrent: () => (source.getCurrent() as CurrentOrganization)?.id ?? undefined,\n }\n }, [instance])\n\n return useSyncExternalStore(subscribe, getCurrent)\n}\n","import {getTopicState, resolveTopic} from '@sanity/sdk/_internal'\nimport {type StateTopic, type TopicData} from '@sanity/sdk/dashboard'\n\nimport {createStateSourceHook} from '../helpers/createStateSourceHook'\n\n/**\n * Returns the current value of a dashboard state topic and follows later updates.\n *\n * The hook suspends until the topic publishes its first value, using the message bus query\n * deadline. A topic declared with `TopicResult` resolves to its successful value; a\n * failed result throws a `TopicError` to the nearest error boundary.\n *\n * @example\n * ```tsx\n * function ForegroundApplication() {\n * const foregroundId = useTopic('applications.foreground')\n * return <span>{foregroundId ?? 'No application in the foreground'}</span>\n * }\n * ```\n *\n * @public\n */\nexport const useTopic = createStateSourceHook({\n getState: getTopicState,\n // `getCurrent` throws a recorded read failure, which surfaces it from render like a thrown value.\n shouldSuspend: (instance, topic: StateTopic) =>\n getTopicState(instance, topic).getCurrent() === undefined,\n suspender: resolveTopic,\n}) as <K extends StateTopic>(topic: K) => TopicData<K>\n","/* eslint-disable react-compiler/react-compiler -- the transport branch in `useStudioWorkspacesByProjectIdDataset` is a deliberate rules-of-hooks exception; the compiler refuses files that disable it */\nimport {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'\nimport {getApplicationOrigin, isDashboardEnvironment} from '@sanity/sdk/_internal'\nimport {type TopicData} from '@sanity/sdk/dashboard'\nimport {useEffect, useMemo, useState} from 'react'\n\nimport {useWindowConnection} from '../comlink/useWindowConnection'\nimport {useTopic} from './useTopic'\n\nexport interface DashboardResource {\n id: string\n name: string\n title: string\n basePath: string\n projectId: string\n dataset: string\n type: string\n userApplicationId: string\n url: string\n}\n\ninterface WorkspacesByProjectIdDataset {\n [key: `${string}:${string}`]: DashboardResource[] // key format: `${projectId}:${dataset}`\n}\n\ninterface StudioWorkspacesResult {\n workspacesByProjectIdAndDataset: WorkspacesByProjectIdDataset\n error: string | null\n}\n\ntype DashboardApplications = NonNullable<TopicData<'applications.list'>>\n\n/**\n * Hook that fetches studio workspaces and organizes them by projectId:dataset\n *\n * Works in both Dashboard runtimes: it derives workspaces from the `applications.list` message\n * bus topic when a host has installed the bus, and falls back to the Comlink connection otherwise.\n * @internal\n *\n * @example\n * ```tsx\n * import {useStudioWorkspacesByProjectIdDataset} from '@sanity/sdk-react'\n * import {Card, Code, Button} from '@sanity/ui'\n * import {Suspense} from 'react'\n *\n * function WorkspacesCard() {\n * const {workspacesByProjectIdAndDataset, error} = useStudioWorkspacesByProjectIdDataset()\n * if (error) {\n * return <div>Error: {error}</div>\n * }\n * return (\n * <Card padding={4} radius={2} shadow={1}>\n * <Code language=\"json\">\n * {JSON.stringify(workspacesByProjectIdAndDataset, null, 2)}\n * </Code>\n * </Card>\n * )\n * }\n *\n * // Wrap the component with Suspense since the hook may suspend\n * function DashboardWorkspaces() {\n * return (\n * <Suspense fallback={<Button text=\"Loading...\" disabled />}>\n * <WorkspacesCard />\n * </Suspense>\n * )\n * }\n * ```\n */\nexport function useStudioWorkspacesByProjectIdDataset(): StudioWorkspacesResult {\n // The branch is stable: the transport is fixed for the page lifetime, so one set of hooks\n // always runs and the other never does.\n // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime\n if (isDashboardEnvironment()) return useBusStudioWorkspaces()\n // eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime\n return useComlinkStudioWorkspaces()\n}\n\n// The legacy Comlink protocol models studios at the workspace level, so each workspace of a\n// studio's active deployment becomes one resource, addressed by the studio's origin.\nfunction toResources(application: DashboardApplications[number]): DashboardResource[] {\n if (application.type !== 'studio') return []\n const url = getApplicationOrigin(application) ?? ''\n return (application.activeDeployment?.workspaces ?? []).map((workspace) => ({\n id: workspace.id,\n name: workspace.name,\n title: workspace.title ?? application.title,\n basePath: workspace.basePath ?? '',\n projectId: workspace.projectId,\n dataset: workspace.dataset,\n type: 'studio',\n userApplicationId: application.id,\n url,\n }))\n}\n\nfunction toWorkspaceMap(applications: DashboardApplications): WorkspacesByProjectIdDataset {\n const workspaceMap: WorkspacesByProjectIdDataset = {}\n for (const resource of applications.flatMap(toResources)) {\n const key = `${resource.projectId}:${resource.dataset}` as const\n workspaceMap[key] ??= []\n workspaceMap[key].push(resource)\n }\n return workspaceMap\n}\n\n// Suspends until the host publishes its application list and throws a `TopicError` on failure,\n// like every bus hook, so `error` is always `null` on this path.\nfunction useBusStudioWorkspaces(): StudioWorkspacesResult {\n const applications = useTopic('applications.list')\n const workspacesByProjectIdAndDataset = useMemo(\n () => toWorkspaceMap(applications ?? []),\n [applications],\n )\n return {workspacesByProjectIdAndDataset, error: null}\n}\n\nfunction useComlinkStudioWorkspaces(): StudioWorkspacesResult {\n const [workspacesByProjectIdAndDataset, setWorkspacesByProjectIdAndDataset] =\n useState<WorkspacesByProjectIdDataset>({})\n const [error, setError] = useState<string | null>(null)\n\n const {fetch} = useWindowConnection({\n name: SDK_NODE_NAME,\n connectTo: SDK_CHANNEL_NAME,\n })\n\n // Once computed, this should probably be in a store and poll for changes\n // However, our stores are currently being refactored\n useEffect(() => {\n if (!fetch) return\n\n async function fetchWorkspaces(signal: AbortSignal) {\n try {\n const data = await fetch<{\n context: {availableResources: Array<DashboardResource>}\n }>('dashboard/v1/context', undefined, {signal})\n\n const workspaceMap: WorkspacesByProjectIdDataset = {}\n const noProjectIdAndDataset: DashboardResource[] = []\n\n data.context.availableResources.forEach((resource) => {\n if (resource.type !== 'studio') return\n if (!resource.projectId || !resource.dataset) {\n noProjectIdAndDataset.push(resource)\n return\n }\n const key = `${resource.projectId}:${resource.dataset}` as const\n if (!workspaceMap[key]) {\n workspaceMap[key] = []\n }\n workspaceMap[key].push(resource)\n })\n\n if (noProjectIdAndDataset.length > 0) {\n workspaceMap['NO_PROJECT_ID:NO_DATASET'] = noProjectIdAndDataset\n }\n\n setWorkspacesByProjectIdAndDataset(workspaceMap)\n setError(null)\n } catch (err: unknown) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return\n }\n setError('Failed to fetch workspaces')\n }\n }\n }\n\n const controller = new AbortController()\n fetchWorkspaces(controller.signal)\n\n return () => {\n controller.abort()\n }\n }, [fetch])\n\n return {\n workspacesByProjectIdAndDataset,\n error,\n }\n}\n"],"mappings":";;;;;;;;;;;;AAGA,MAAaE,wBAAwBD,cAAqC,IAAI,GCsBjEK,0BAAoB;CAC/B,IAAAC,WAAiBH,WAAWC,qBAAqB;CAEjD,IAAI,CAACE,UACH,MAAUC,MACR,gIACF;CACD,OAEMD;AAAQ;ACjBjB,SAAgBkB,sBACdC,SACgC;CAChC,IAAMN,WAAW,OAAOM,WAAY,aAAaA,UAAUA,QAAQN,UAC7DO,WAAW,mBAAmBD,WAAW,eAAeA,UAAUA,UAAUE,KAAAA;CAElF,SAAAC,QAAA,GAAAC,IAAA;EAAA,IAAAC,IAAAC,EAAA,CAAA,GAAiBhB,SAAAc,IACff,WAAiBF,kBAAkB;EAEnC,IAAIc,UAAQL,aAAeK,UAAQN,gBAAkBN,UAAQ,GAAKC,MAAM,GACtE,MAAMW,SAAQL,UAAWP,UAAQ,GAAKC,MAAM;EAC7C,IAAAiB;EAAA,AAAAF,EAAA,OAAAhB,YAAAgB,EAAA,OAAAf,UAEaiB,KAAAb,SAASL,UAAQ,GAAKC,MAAM,GAACe,EAAA,KAAAhB,UAAAgB,EAAA,KAAAf,QAAAe,EAAA,KAAAE,MAAAA,KAAAF,EAAA;EAA3C,IAAAG,QAAcD;EAA6B,OACpCrB,qBAAqBsB,MAAKC,WAAYD,MAAKE,UAAW;CAAC;CAGhE,OAAOP;AACT;;;;;;;;;;;;;;;;;;;;;;ACVA,MAAaW,eAAgCD,sBAAsBD,YAAY,GCwBzEyC,eAAexB,sBAAsB;CACzCyB,UAAUlC;CAIVqC,gBAAgBF,UAA0BC,cACxCpC,aAAamC,UAAUC,SAAS,CAAC,CAACE,WAAW,MAAMC,KAAAA;CACrDC,YAAYL,UAA0BC,cAC7B7B,eAAeP,aAAamC,UAAUC,SAAS,CAAC,CAACK,WAAWC,KAAKpC,OAAOqC,OAAO,CAAC,CAAC;AAE5F,CAAC;;;;;;;;;;AAWD,SAAOC,oBAAAC,IAAA;CAAA,IAAAC,IAAAC,EAAA,EAAA,GAGL,EAAAjC,MAAAC,WAAAC,cAAA6B,IAI0CG;CAAA,AAAAF,EAAA,OAAA/B,aAAA+B,EAAA,OAAAhC,QACdkC,KAAA;EAAAlC;EAAAC;CAAgB,GAAC+B,EAAA,KAAA/B,WAAA+B,EAAA,KAAAhC,MAAAgC,EAAA,KAAAE,MAAAA,KAAAF,EAAA;CAA7C,IAAA,EAAAG,SAAehB,aAAae,EAAiB,GAACE;CAAA,AAAAJ,EAAA,OAAAK,OAAAC,IAAA,2BAAA,KACMF,KAAA,CAAA,GAAEJ,EAAA,KAAAI,MAAAA,KAAAJ,EAAA;CAAtD,IAAAO,uBAA6BhD,OAAuB6C,EAAE,GACtDf,WAAiB3B,kBAAkB,GAAC8C;CAAA,AAAAR,EAAA,OAAAG,QAAAH,EAAA,OAAA9B,aAE1BsC,YACJtC,aACFuC,OAAMC,QAASxC,SAAS,CAAC,CAAAyC,SAASC,OAAA;EAAC,IAAA,CAAArC,MAAAsC,WAAAD,IACjCE,qBAA2BX,KAAIY,GAAIxC,MAAMsC,OAA8C;EACvF,AAAIC,sBACFP,qBAAoBS,QAAQC,KAAMH,kBAAkB;CACrD,CACF,SAGI;EAELP,AADAA,qBAAoBS,QAAQL,QAASO,OAA8B,GACnEX,qBAAoBS,UAAW,CAAA;CAAH,IAE/BhB,EAAA,KAAAG,MAAAH,EAAA,KAAA9B,WAAA8B,EAAA,KAAAQ,MAAAA,KAAAR,EAAA;CAAA,IAAAY;CAdDtD,AAcC0C,EAAA,OAAAX,YAAAW,EAAA,OAAAhC,QAAAgC,EAAA,OAAAG,QAAAH,EAAA,QAAA9B,aAAE0C,KAAA;EAACvB;EAAUrB;EAAME;EAAWiC;CAAI,GAACH,EAAA,KAAAX,UAAAW,EAAA,KAAAhC,MAAAgC,EAAA,KAAAG,MAAAH,EAAA,MAAA9B,WAAA8B,EAAA,MAAAY,MAAAA,KAAAZ,EAAA,KAdpC1C,UAAUkD,IAcPI,EAAiC;CAAC,IAAAO;CAAA,AAAAnB,EAAA,QAAAG,OAKlCgB,KAAAnB,EAAA,OAFDmB,MAAAC,QAAA3C,SAAA;EACE0B,KAAIkB,KAAM9C,QAAME,IAAI;CAAC,GACtBuB,EAAA,MAAAG,MAAAH,EAAA,MAAAmB;CAHH,IAAA7C,cAAoB6C,IAKnBG;CAAA,AAAAtB,EAAA,QAAAG,OAaEmB,KAAAtB,EAAA,OAVDsB,MAAAC,QAAAC,QAAAC,iBASStB,KAAIxB,MAAOJ,QAAME,QAAMgD,gBAAA,CAAiB,CAAC,GACjDzB,EAAA,MAAAG,MAAAH,EAAA,MAAAsB;CAXH,IAAA3C,QAAc2C,IAabI;CAIA,OAJA1B,EAAA,QAAArB,SAAAqB,EAAA,QAAA1B,eACMoD,KAAA;EAAApD;EAAAK;CAGP,GAACqB,EAAA,MAAArB,OAAAqB,EAAA,MAAA1B,aAAA0B,EAAA,MAAA0B,MAAAA,KAAA1B,EAAA,KAHM0B;AAGN;AApDI,SAAAR,QAAAS,aAAA;CAAA,OAuBqDA,YAAY;AAAC;AC3FzE,MAAME,kBAAkBC,OAAOC,IAAI,kBAAkB;;;;;;;;;;;;;;;;;;;;;;AA0BrD,SAAgBM,4BAA2C;CACzD,IAAMC,UAAUC,YACVC,OAAQF,QAAQT,qCAAqB,IAAIM,QAAQ,GACjDM,MAAMb,QAAMQ,eACdM,UAAUF,KAAKG,IAAIF,GAAG;CAK1B,OAJKC,YACHA,UAAUd,QAAMQ,cAAkCQ,KAAAA,CAAS,GAC3DJ,KAAKK,IAAIJ,KAAKC,OAAO,IAEhBA;AACT;;;;;;;;;;;AChBA,SAAAqB,sBAAAC,IAAA;CAAA,IAAAC,IAAAC,EAAA,EAAA,GAA+B,EAAAC,UAAAC,eAAAJ,IAI7BK,WAAiBP,kBAAkB,GACnCQ,YAAkBT,aAAa,GAC/BU,uBAA6BhB,OAAuB,IAAI,GAACiB,IAAAC;CAEzDnB,AAFyDW,EAAA,OAAAI,YAAAJ,EAAA,OAAAG,cAE/CI,WAAA;EACR,IAAAE,eAAqBjB,YAAYW,WAAUO,UAAW,YAAY,CAAC,CAAC,CAAAC,KAC5DjB,WAAWkB,KAAc,CAAC,CAAC,CAAAF,WACtBG,UAAW9B,aAAaqB,UAAUS,KAAK,CAAC;EAAC,aACzCJ,aAAYK,YAAa;CAAC,GACtCN,KAAA,CAACJ,UAAUD,UAAU,GAACH,EAAA,KAAAI,UAAAJ,EAAA,KAAAG,YAAAH,EAAA,KAAAO,IAAAP,EAAA,KAAAQ,OAAAD,KAAAP,EAAA,IAAAQ,KAAAR,EAAA,KALzBX,UAAUkB,IAKPC,EAAsB;CAAC,IAAAO;CAAA,AAAAf,EAAA,OAAAK,UAAAW,SAAAhB,EAAA,OAAAK,UAAAY,QAAAjB,EAAA,OAAAG,cAEhBY,WAAA;EACR,IAAAG,cACEb,UAASY,SAAUnC,cAAaqC,SAAWd,UAASW,OAAkCI,eAAK;EAE7F,AAAIF,eAAeZ,qBAAoBe,YAAahB,UAASW,SAC3DV,qBAAoBe,UAAWhB,UAASW,OAExCb,WAAUmB,KAAM,sBAAsBC,KAAAA,CAAS,CAAC,CAAAC,MAAOC,MAGtD,KACSP,gBACVZ,qBAAoBe,UAAW;CAChC,GACFrB,EAAA,KAAAK,UAAAW,OAAAhB,EAAA,KAAAK,UAAAY,MAAAjB,EAAA,KAAAG,YAAAH,EAAA,KAAAe,MAAAA,KAAAf,EAAA;CAAA,IAAA0B;CAA0B,OAA1B1B,EAAA,OAAAK,aAAAL,EAAA,OAAAG,cAAEuB,KAAA,CAACrB,WAAWF,UAAU,GAACH,EAAA,KAAAK,WAAAL,EAAA,KAAAG,YAAAH,EAAA,MAAA0B,MAAAA,KAAA1B,EAAA,KAd1BX,UAAU0B,IAcPW,EAAuB,GAEnBxB;AAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA/BjB,SAAAuB,OAAAT,OAAA;CAwBQW,QAAOC,KAAM,gDAAgDZ,KAAK;AAAC;AAxB3E,SAAAJ,QAAA;CAAA,OAU6BnB,GAAG,IAAI;AAAC;AA+DrC,MAAaoC,iCAA6D9B,OAAA;CAAA,IAAAC,IAAAC,EAAA,CAAA,GAAC,EAAAC,aAAAH,IACzEK,WAAiBP,kBAAkB,GAACU;CAAA,AAAAP,EAAA,OAAA+B,OAAAC,IAAA,2BAAA,KACRzB,KAAAZ,0BAA0B,GAACK,EAAA,KAAAO,MAAAA,KAAAP,EAAA;CAAvD,IAAAiC,WAAiB7C,WAAWmB,EAA2B,GAACC;CAAA,AAAAR,EAAA,OAAAI,YAAAJ,EAAA,OAAAiC,YAO1BzB,WAAMxB,uBAAuBoB,UAAU6B,QAAQ,GAACjC,EAAA,KAAAI,UAAAJ,EAAA,KAAAiC,UAAAjC,EAAA,KAAAQ,MAAAA,KAAAR,EAAA;CAA9E,IAAA,CAAAG,cAAqBZ,SAASiB,EAAgD;CAC9E,IAAIL,YAAU;EAAA,IAAAY;EAC4E,OAD5Ef,EAAA,OAAAE,YAAAF,EAAA,OAAAG,cACLY,KAAA,oBAAC,uBAAD;GAAmCZ;GAAaD;EAA1B,CAAA,GAA2DF,EAAA,KAAAE,UAAAF,EAAA,KAAAG,YAAAH,EAAA,KAAAe,MAAAA,KAAAf,EAAA,IAAjFe;CAAiF;CACzF,OAEMb;AAAQ;;;;;;;;;;;;;;;;;;;;;;;AC/EjB,SAAOyC,oBAAA;CAAA,IAAAC,IAAAC,EAAA,CAAA,GACLC,WAAiBN,kBAAkB,GAACO;CAAAC,KAAA;EAElC,IAAI,CAACX,uBAAuB,GAAC;GAAA,IAAAY;GAAEF,AAAFH,EAAA,OAAAE,WAA6CG,KAAAL,EAAA,MAApCK,KAAAf,2BAA2BY,QAAQ,GAACF,EAAA,KAAAE,UAAAF,EAAA,KAAAK,KAA3CF,KAAOE;GAAP,MAAAD;EAA2C;EAAA,IAAAC;EAAA,AAAAL,EAAA,OAAAE,WACXG,KAAAL,EAAA,MAAhDK,KAAAb,cAAcU,UAAU,uBAAuB,GAACF,EAAA,KAAAE,UAAAF,EAAA,KAAAK;EAA/D,IAAAC,SAAeD,IAAgDE;EAAA,AAAAP,EAAA,OAAAM,SAGkBC,KAAAP,EAAA,MAAnEO,WAAOD,OAAME,WAAY,CAAC,EAA4BC,MAAhDC,KAAAA,GAA6DV,EAAA,KAAAM,QAAAN,EAAA,KAAAO;EAAA,IAAAI;EAFjFR,AAEiFH,EAAA,OAAAM,OAAAM,aAAAZ,EAAA,OAAAO,MAF1EI,KAAA;GAAAC,WACMN,OAAMM;GAAUJ,YACfD;EACd,GAACP,EAAA,KAAAM,OAAAM,WAAAZ,EAAA,KAAAO,IAAAP,EAAA,KAAAW,MAAAA,KAAAX,EAAA,IAHDG,KAAOQ;CAGN;CANH,IAAA,EAAAC,WAAAJ,eAAgCL;CAOlB,OAEPR,qBAAqBiB,WAAWJ,UAAU;AAAC;;;;;;;;;;;;;;;;;;ACnBpD,MAAaU,WAAWD,sBAAsB;CAC5CE,UAAUN;CAEVO,gBAAgBC,UAAUC,UACxBT,cAAcQ,UAAUC,KAAK,CAAC,CAACC,WAAW,MAAMC,KAAAA;CAClDC,WAAWX;AACb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACyCD,SAAgBwC,wCAAgE;CAM9E,OAFIxB,uBAAuB,IAAUyB,uBAAuB,IAErDC,2BAA2B;AACpC;AAIA,SAASC,YAAYC,aAAiE;CACpF,IAAIA,YAAYd,SAAS,UAAU,OAAO,CAAA;CAC1C,IAAME,MAAMjB,qBAAqB6B,WAAW,KAAK;CACjD,QAAQA,YAAYC,kBAAkBC,cAAc,CAAA,EAAA,CAAIC,KAAKC,eAAe;EAC1ExB,IAAIwB,UAAUxB;EACdC,MAAMuB,UAAUvB;EAChBC,OAAOsB,UAAUtB,SAASkB,YAAYlB;EACtCC,UAAUqB,UAAUrB,YAAY;EAChCC,WAAWoB,UAAUpB;EACrBC,SAASmB,UAAUnB;EACnBC,MAAM;EACNC,mBAAmBa,YAAYpB;EAC/BQ;CACF,EAAE;AACJ;AAEA,SAASiB,eAAeC,cAAmE;CACzF,IAAMC,eAA6C,CAAC;CACpD,KAAK,IAAMC,YAAYF,aAAaG,QAAQV,WAAW,GAAG;EACxD,IAAMT,MAAM,GAAGkB,SAASxB,UAAS,GAAIwB,SAASvB;EAE9CsB,AADAA,aAAajB,SAAS,CAAA,GACtBiB,aAAajB,IAAI,CAACqB,KAAKH,QAAQ;CACjC;CACA,OAAOD;AACT;AAIA,SAAAV,yBAAA;CAAA,IAAAe,IAAAC,EAAA,CAAA,GACEP,eAAqB5B,SAAS,mBAAmB,GAACoC;CAAA,AAAAF,EAAA,OAAAN,eAETQ,KAAAF,EAAA,MAAlBE,KAAAR,gBAAA,CAAA,GAAkBM,EAAA,KAAAN,cAAAM,EAAA,KAAAE;CAAA,IAAAC;CAAA,AAAAH,EAAA,OAAAE,KAACC,KAAAH,EAAA,MAAlCG,KAAAV,eAAeS,EAAkB,GAACF,EAAA,KAAAE,IAAAF,EAAA,KAAAG;CAD1C,IAAAvB,kCACQuB,IAEPC;CACoD,OADpDJ,EAAA,OAAApB,kCACoDwB,KAAAJ,EAAA,MAA9CI,KAAA;EAAAxB;EAAAC,OAAyC;CAAI,GAACmB,EAAA,KAAApB,iCAAAoB,EAAA,KAAAI,KAA9CA;AAA8C;AAGvD,SAAAlB,6BAAA;CAAA,IAAAc,IAAAC,EAAA,CAAA,GAAAC;CAAA,AAAAF,EAAA,OAAAK,OAAAC,IAAA,2BAAA,KAE2CJ,KAAA,CAAC,GAACF,EAAA,KAAAE,MAAAA,KAAAF,EAAA;CAD3C,IAAA,CAAApB,iCAAA2B,sCACE3C,SAAuCsC,EAAE,GAC3C,CAAArB,OAAA2B,YAA0B5C,SAAwB,IAAI,GAACuC;CAAA,AAAAH,EAAA,OAAAK,OAAAC,IAAA,2BAAA,KAEnBH,KAAA;EAAAlC,MAC5BX;EAAamD,WACRpD;CACb,GAAC2C,EAAA,KAAAG,MAAAA,KAAAH,EAAA;CAHD,IAAA,EAAAU,UAAgB7C,oBAAoBsC,EAGnC,GAACC,IAAAO;CAIFjD,AAJEsC,EAAA,OAAAU,SAmDQN,KAAAJ,EAAA,IAAAW,KAAAX,EAAA,OA/CAI,WAAA;EACR,IAAI,CAACM,OAAK;EAEV,IAAAE,kBAAA,eAAAA,gBAAAC,QAAA;GACE,IAAA;IACE,IAAAC,OAAa,MAAMJ,MAEhB,wBAAwBK,KAAAA,GAAW,EAAAF,OAAO,CAAC,GAE9ClB,eAAmD,CAAC,GACpDqB,wBAAmD,CAAA;IAoBnDR,AAlBAM,KAAIG,QAAQC,mBAAmBC,SAASvB,aAAA;KACtC,IAAIA,SAAQtB,SAAU,UAAQ;KAC9B,IAAI,CAACsB,SAAQxB,aAAT,CAAwBwB,SAAQvB,SAAQ;MAC1C2C,sBAAqBjB,KAAMH,QAAQ;MAAC;KAAA;KAGtC,IAAAlB,MAAY,GAAGkB,SAAQxB,UAAU,GAAIwB,SAAQvB;KAI7CsB,AAHKA,aAAajB,SAChBiB,aAAajB,OAAO,CAAA,IAEtBiB,aAAajB,IAAI,CAAAqB,KAAMH,QAAQ;IAAC,CACjC,GAEGoB,sBAAqBI,SAAU,MACjCzB,aAAa,8BAA8BqB,wBAG7CT,mCAAmCZ,YAAY,GAC/Ca,SAAS,IAAI;GAAC,SAAAa,IAAA;IACPC,IAAAA,MAAAA;IACP,IAAIA,eAAeC,OAAK;KACtB,IAAID,IAAGrD,SAAU,cAAY;KAG7BuC,SAAS,4BAA4B;IAAC;GACvC;EACF,GAGHgB,aAAmB,IAAIC,gBAAgB;EACL,OAAlCb,gBAAgBY,WAAUX,MAAO,SAE1B;GACLW,WAAUE,MAAO;EAAC;CACnB,GACAf,KAAA,CAACD,KAAK,GAACV,EAAA,KAAAU,OAAAV,EAAA,KAAAI,IAAAJ,EAAA,KAAAW,KA/CVjD,UAAU0C,IA+CPO,EAAO;CAAC,IAAAU;CAKV,OALUrB,EAAA,OAAAnB,SAAAmB,EAAA,OAAApB,mCAEJyC,KAAA;EAAAzC;EAAAC;CAGP,GAACmB,EAAA,KAAAnB,OAAAmB,EAAA,KAAApB,iCAAAoB,EAAA,KAAAqB,MAAAA,KAAArB,EAAA,IAHMqB;AAGN"}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import {handleOAuthCallback} from '@sanity/sdk'
|
|
2
|
-
import {identity} from 'rxjs'
|
|
3
|
-
import {describe, it} from 'vitest'
|
|
4
|
-
|
|
5
|
-
import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
6
|
-
|
|
7
|
-
vi.mock('../helpers/createCallbackHook', () => ({createCallbackHook: vi.fn(identity)}))
|
|
8
|
-
vi.mock('@sanity/sdk', () => ({handleOAuthCallback: vi.fn()}))
|
|
9
|
-
|
|
10
|
-
describe('useHandleOAuthCallback', () => {
|
|
11
|
-
it('calls `createCallbackHook` with `handleOAuthCallback`', async () => {
|
|
12
|
-
const {useHandleOAuthCallback} = await import('./useHandleOAuthCallback')
|
|
13
|
-
expect(createCallbackHook).toHaveBeenCalledWith(handleOAuthCallback)
|
|
14
|
-
expect(useHandleOAuthCallback).toBe(handleOAuthCallback)
|
|
15
|
-
})
|
|
16
|
-
})
|