@sanity/sdk-react 3.1.0 → 3.3.0-rc.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-internal.d.ts +2 -0
- package/dist/_exports/dashboard-internal.js +2 -0
- package/dist/_exports/dashboard.d.ts +457 -7
- package/dist/_exports/dashboard.d.ts.map +1 -1
- package/dist/_exports/dashboard.js +623 -2
- package/dist/_exports/dashboard.js.map +1 -1
- package/dist/index.d.ts +227 -87
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -30
- package/dist/index.js.map +1 -1
- package/dist/{useStudioWorkspacesByProjectIdDataset-DxUlukmF.js → useStudioWorkspacesByProjectIdDataset-BZ_Ud887.js} +53 -56
- package/dist/useStudioWorkspacesByProjectIdDataset-BZ_Ud887.js.map +1 -0
- package/package.json +34 -32
- package/src/_exports/dashboard-internal.ts +1 -0
- package/src/_exports/dashboard.test-d.ts +64 -0
- package/src/_exports/dashboard.ts +70 -0
- package/src/_exports/index.ts +1 -1
- package/src/_exports/sdk-react.ts +3 -0
- package/src/components/auth/AuthBoundary.test.tsx +115 -2
- package/src/components/auth/AuthBoundary.tsx +18 -5
- package/src/components/auth/LoginCallback.test.tsx +46 -7
- package/src/components/auth/LoginCallback.tsx +22 -4
- package/src/context/DashboardTokenRefresh.test.tsx +124 -22
- package/src/context/DashboardTokenRefresh.tsx +31 -15
- package/src/dashboard/createRemoteInstance.test.ts +168 -0
- package/src/dashboard/createRemoteInstance.ts +123 -0
- package/src/dashboard/module.ts +39 -0
- package/src/dashboard/remoteClientState.ts +27 -0
- package/src/dashboard/urlFor.test.ts +246 -0
- package/src/dashboard/urlFor.ts +462 -0
- package/src/hooks/applications/useApplication.test-d.ts +1 -1
- package/src/hooks/auth/useHandleOAuthCallback.test.tsx +16 -0
- package/src/hooks/auth/useHandleOAuthCallback.tsx +49 -0
- package/src/hooks/auth/useOAuthAuthorize.test.tsx +16 -0
- package/src/hooks/auth/useOAuthAuthorize.tsx +28 -0
- package/src/hooks/auth/useOAuthTokens.test.tsx +240 -0
- package/src/hooks/auth/useOAuthTokens.tsx +95 -0
- package/src/hooks/dashboard/useApplication.test.tsx +59 -0
- package/src/hooks/dashboard/useApplication.ts +22 -0
- package/src/hooks/dashboard/useApplicationConfig.test.tsx +106 -0
- package/src/hooks/dashboard/useApplicationConfig.ts +45 -0
- package/src/hooks/dashboard/useApplicationConfigs.test.tsx +89 -0
- package/src/hooks/dashboard/useApplicationConfigs.ts +26 -0
- package/src/hooks/dashboard/useApplicationForegroundId.test.tsx +54 -0
- package/src/hooks/dashboard/useApplicationForegroundId.ts +22 -0
- package/src/hooks/dashboard/useApplications.test.tsx +249 -0
- package/src/hooks/dashboard/useApplications.ts +128 -0
- package/src/hooks/dashboard/useEmit.test.tsx +124 -0
- package/src/hooks/dashboard/useEmit.ts +78 -0
- package/src/hooks/dashboard/useNavigate.ts +1 -3
- package/src/hooks/dashboard/useRemoteClient.test.tsx +88 -0
- package/src/hooks/dashboard/useRemoteClient.ts +10 -0
- package/src/hooks/dashboard/useTopic.test.tsx +200 -0
- package/src/hooks/dashboard/useTopic.ts +29 -0
- package/src/hooks/dashboard/useUpdateFavorite.test.tsx +8 -1
- package/src/hooks/document/useApplyDocumentActions.ts +3 -4
- package/src/hooks/document/useCreateDocument.ts +2 -3
- package/src/hooks/document/useDocument.ts +11 -6
- package/src/hooks/document/useEditDocument.ts +5 -5
- package/src/hooks/projection/useDocumentProjection.ts +6 -7
- package/src/hooks/query/useQuery.ts +3 -4
- package/dist/useStudioWorkspacesByProjectIdDataset-DxUlukmF.js.map +0 -1
- package/src/context/dashboardToken.ts +0 -63
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {type ReactNode, StrictMode} from 'react'
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {renderHook} from '../../../test/test-utils'
|
|
6
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
7
|
+
import {useRemoteClient} from './useRemoteClient'
|
|
8
|
+
|
|
9
|
+
const {createMFInstance} = vi.hoisted(() => ({
|
|
10
|
+
createMFInstance: vi.fn(() => ({
|
|
11
|
+
registerRemotes: vi.fn(),
|
|
12
|
+
loadRemote: vi.fn(),
|
|
13
|
+
preloadRemote: vi.fn(),
|
|
14
|
+
})),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
vi.mock('@module-federation/runtime', () => ({createInstance: createMFInstance}))
|
|
18
|
+
|
|
19
|
+
function wrapper(instance: SanityInstance) {
|
|
20
|
+
return function TestProvider({children}: {children: ReactNode}) {
|
|
21
|
+
return (
|
|
22
|
+
<StrictMode>
|
|
23
|
+
<SanityInstanceContext.Provider value={instance}>{children}</SanityInstanceContext.Provider>
|
|
24
|
+
</StrictMode>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('useRemoteClient', () => {
|
|
30
|
+
const instances: SanityInstance[] = []
|
|
31
|
+
function createTestInstance() {
|
|
32
|
+
const instance = createSanityInstance()
|
|
33
|
+
instances.push(instance)
|
|
34
|
+
return instance
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beforeEach(() => vi.clearAllMocks())
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
instances.splice(0).forEach((instance) => instance.dispose())
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("creates a client for the hook's Sanity instance", () => {
|
|
43
|
+
const instance = createTestInstance()
|
|
44
|
+
renderHook(() => useRemoteClient(), {wrapper: wrapper(instance)})
|
|
45
|
+
|
|
46
|
+
expect(createMFInstance).toHaveBeenCalledTimes(1)
|
|
47
|
+
expect(createMFInstance).toHaveBeenCalledWith(
|
|
48
|
+
expect.objectContaining({
|
|
49
|
+
name: `sanity-remote-${instance.instanceId}`,
|
|
50
|
+
remotes: [],
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('does not create a client for a disposed Sanity instance', () => {
|
|
56
|
+
const instance = createTestInstance()
|
|
57
|
+
instance.dispose()
|
|
58
|
+
|
|
59
|
+
expect(() => renderHook(() => useRemoteClient(), {wrapper: wrapper(instance)})).toThrow(
|
|
60
|
+
'Cannot create a remote client for a disposed Sanity instance',
|
|
61
|
+
)
|
|
62
|
+
expect(createMFInstance).not.toHaveBeenCalled()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('shares a client across consumers and remounts using the same instance', () => {
|
|
66
|
+
const instance = createTestInstance()
|
|
67
|
+
const first = renderHook(() => useRemoteClient(), {wrapper: wrapper(instance)})
|
|
68
|
+
const second = renderHook(() => useRemoteClient(), {wrapper: wrapper(instance)})
|
|
69
|
+
expect(second.result.current).toBe(first.result.current)
|
|
70
|
+
const client = first.result.current
|
|
71
|
+
first.unmount()
|
|
72
|
+
second.unmount()
|
|
73
|
+
|
|
74
|
+
const remounted = renderHook(() => useRemoteClient(), {wrapper: wrapper(instance)})
|
|
75
|
+
expect(remounted.result.current).toBe(client)
|
|
76
|
+
expect(createMFInstance).toHaveBeenCalledTimes(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('isolates clients between Sanity instances', () => {
|
|
80
|
+
const firstInstance = createTestInstance()
|
|
81
|
+
const secondInstance = createTestInstance()
|
|
82
|
+
const first = renderHook(() => useRemoteClient(), {wrapper: wrapper(firstInstance)})
|
|
83
|
+
const second = renderHook(() => useRemoteClient(), {wrapper: wrapper(secondInstance)})
|
|
84
|
+
|
|
85
|
+
expect(first.result.current).not.toBe(second.result.current)
|
|
86
|
+
expect(createMFInstance).toHaveBeenCalledTimes(2)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {type RemoteInstance} from '../../dashboard/createRemoteInstance'
|
|
2
|
+
import {getRemoteClientState} from '../../dashboard/remoteClientState'
|
|
3
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a remote client shared by hooks using the same Sanity instance.
|
|
7
|
+
* The first call creates the client; later calls reuse it.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export const useRemoteClient: () => RemoteInstance = createStateSourceHook(getRemoteClientState)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
|
|
2
|
+
import {
|
|
3
|
+
MessageBusError,
|
|
4
|
+
type MessageBusHost,
|
|
5
|
+
type TopicData,
|
|
6
|
+
TopicError,
|
|
7
|
+
type ValueOf,
|
|
8
|
+
} from '@sanity/sdk/dashboard'
|
|
9
|
+
import {Suspense} from 'react'
|
|
10
|
+
import {ErrorBoundary} from 'react-error-boundary'
|
|
11
|
+
import {afterEach, beforeEach, describe, expect, expectTypeOf, it, vi} from 'vitest'
|
|
12
|
+
|
|
13
|
+
import {act, fireEvent, render, renderHook, screen} from '../../../test/test-utils'
|
|
14
|
+
import {useTopic} from './useTopic'
|
|
15
|
+
|
|
16
|
+
type Applications = Extract<NonNullable<ValueOf<'applications.list'>>, {ok: true}>['value']
|
|
17
|
+
|
|
18
|
+
const MESSAGE_BUS_KEY = Symbol.for('sanity.os.bus')
|
|
19
|
+
|
|
20
|
+
let host: MessageBusHost
|
|
21
|
+
|
|
22
|
+
function Token() {
|
|
23
|
+
return <span>{useTopic('auth.token')}</span>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ApplicationCount() {
|
|
27
|
+
const applications = useTopic('applications.list')
|
|
28
|
+
return <span>{applications?.length ?? 0} applications</span>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderInBoundary(ui: React.ReactNode, onError = vi.fn()) {
|
|
32
|
+
render(
|
|
33
|
+
<ErrorBoundary
|
|
34
|
+
fallbackRender={({resetErrorBoundary}) => <button onClick={resetErrorBoundary}>Retry</button>}
|
|
35
|
+
onError={onError}
|
|
36
|
+
>
|
|
37
|
+
<Suspense fallback="Loading">{ui}</Suspense>
|
|
38
|
+
</ErrorBoundary>,
|
|
39
|
+
)
|
|
40
|
+
return onError
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('useTopic', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
// The SDK resolves its own app ID from the CLI-embedded global.
|
|
46
|
+
vi.stubGlobal('__SANITY_APP_ID__', 'app')
|
|
47
|
+
host = installMessageBus({appId: 'dashboard'})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
resetMessageBus()
|
|
52
|
+
delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
|
|
53
|
+
vi.unstubAllGlobals()
|
|
54
|
+
vi.useRealTimers()
|
|
55
|
+
vi.restoreAllMocks()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('reads a published value and follows topic updates', () => {
|
|
59
|
+
host.connections.subscribe((client) => client.emit('applications.foreground', null))
|
|
60
|
+
const {result} = renderHook(() => useTopic('applications.foreground'))
|
|
61
|
+
|
|
62
|
+
expectTypeOf(result.current).toEqualTypeOf<string | null>()
|
|
63
|
+
expect(result.current).toBeNull()
|
|
64
|
+
|
|
65
|
+
act(() =>
|
|
66
|
+
host.connections.subscribe((client) =>
|
|
67
|
+
client.emit('applications.foreground', 'application-2'),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(result.current).toBe('application-2')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('suspends an unseeded topic until its first value', async () => {
|
|
75
|
+
renderInBoundary(<Token />)
|
|
76
|
+
|
|
77
|
+
expect(screen.getByText('Loading')).toBeInTheDocument()
|
|
78
|
+
|
|
79
|
+
await act(async () => {
|
|
80
|
+
host.connections.subscribe((client) => client.emit('auth.token', 'token'))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(await screen.findByText('token')).toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('unwraps a successful topic result to its value', () => {
|
|
87
|
+
const applications = [{id: 'application-1'}] as Applications
|
|
88
|
+
host.connections.subscribe((client) =>
|
|
89
|
+
client.emit('applications.list', {ok: true, value: applications}),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const {result} = renderHook(() => useTopic('applications.list'))
|
|
93
|
+
|
|
94
|
+
expectTypeOf(result.current).toEqualTypeOf<TopicData<'applications.list'>>()
|
|
95
|
+
expectTypeOf(result.current).toEqualTypeOf<Applications | null>()
|
|
96
|
+
expect(result.current).toBe(applications)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('throws a TopicError to the error boundary when a topic result fails', () => {
|
|
100
|
+
host.connections.subscribe((client) => client.emit('applications.list', {ok: false}))
|
|
101
|
+
|
|
102
|
+
const onError = renderInBoundary(<ApplicationCount />)
|
|
103
|
+
|
|
104
|
+
expect(screen.getByText('Retry')).toBeInTheDocument()
|
|
105
|
+
const error = onError.mock.calls[0][0] as TopicError
|
|
106
|
+
expect(error).toBeInstanceOf(TopicError)
|
|
107
|
+
expect(error.topic).toBe('applications.list')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('throws a TopicError when the first published result fails', async () => {
|
|
111
|
+
const onError = renderInBoundary(<ApplicationCount />)
|
|
112
|
+
expect(screen.getByText('Loading')).toBeInTheDocument()
|
|
113
|
+
|
|
114
|
+
await act(async () => {
|
|
115
|
+
host.connections.subscribe((client) => client.emit('applications.list', {ok: false}))
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
expect(await screen.findByText('Retry')).toBeInTheDocument()
|
|
119
|
+
expect(onError.mock.calls[0][0]).toBeInstanceOf(TopicError)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('throws a TopicError when a later result fails', () => {
|
|
123
|
+
host.connections.subscribe((client) =>
|
|
124
|
+
client.emit('applications.list', {ok: true, value: [] as Applications}),
|
|
125
|
+
)
|
|
126
|
+
const onError = renderInBoundary(<ApplicationCount />)
|
|
127
|
+
expect(screen.getByText('0 applications')).toBeInTheDocument()
|
|
128
|
+
|
|
129
|
+
act(() => host.connections.subscribe((client) => client.emit('applications.list', {ok: false})))
|
|
130
|
+
|
|
131
|
+
expect(screen.getByText('Retry')).toBeInTheDocument()
|
|
132
|
+
expect(onError.mock.calls[0][0]).toBeInstanceOf(TopicError)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('throws to the error boundary when the query deadline passes', async () => {
|
|
136
|
+
// Only the query deadline is faked; React's scheduler keeps real timers so the retry renders.
|
|
137
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']})
|
|
138
|
+
|
|
139
|
+
const onError = renderInBoundary(<Token />)
|
|
140
|
+
expect(screen.getByText('Loading')).toBeInTheDocument()
|
|
141
|
+
|
|
142
|
+
await act(async () => {
|
|
143
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(screen.getByText('Retry')).toBeInTheDocument()
|
|
147
|
+
const error = onError.mock.calls[0][0] as MessageBusError
|
|
148
|
+
expect(error).toBeInstanceOf(MessageBusError)
|
|
149
|
+
expect(error.code).toBe('TIMEOUT')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('retries with a fresh deadline when the boundary resets after the failure is released', async () => {
|
|
153
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']})
|
|
154
|
+
|
|
155
|
+
const onError = renderInBoundary(<Token />)
|
|
156
|
+
await act(async () => {
|
|
157
|
+
await vi.advanceTimersByTimeAsync(5000)
|
|
158
|
+
})
|
|
159
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
160
|
+
|
|
161
|
+
// The failure is held for a short grace period, then dropped with its last reader.
|
|
162
|
+
await act(async () => {
|
|
163
|
+
await vi.advanceTimersByTimeAsync(1000)
|
|
164
|
+
})
|
|
165
|
+
fireEvent.click(screen.getByText('Retry'))
|
|
166
|
+
expect(screen.getByText('Loading')).toBeInTheDocument()
|
|
167
|
+
|
|
168
|
+
await act(async () => {
|
|
169
|
+
host.connections.subscribe((client) => client.emit('auth.token', 'token'))
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
expect(screen.getByText('token')).toBeInTheDocument()
|
|
173
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('does not throw a failure recorded after the reader unmounted at the next mount', async () => {
|
|
177
|
+
vi.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']})
|
|
178
|
+
|
|
179
|
+
const {unmount} = render(<Suspense fallback="Loading">{<Token />}</Suspense>)
|
|
180
|
+
unmount()
|
|
181
|
+
await act(async () => {
|
|
182
|
+
await vi.advanceTimersByTimeAsync(6000)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const onError = renderInBoundary(<Token />)
|
|
186
|
+
|
|
187
|
+
expect(screen.getByText('Loading')).toBeInTheDocument()
|
|
188
|
+
expect(onError).not.toHaveBeenCalled()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('throws when used outside a dashboard application', () => {
|
|
192
|
+
resetMessageBus()
|
|
193
|
+
delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
|
|
194
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
195
|
+
|
|
196
|
+
expect(() => renderHook(() => useTopic('applications.foreground'))).toThrow(
|
|
197
|
+
'Cannot read topic "applications.foreground" without an installed dashboard message bus',
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {getTopicState, resolveTopic} from '@sanity/sdk/_internal'
|
|
2
|
+
import {type StateTopic, type TopicData} from '@sanity/sdk/dashboard'
|
|
3
|
+
|
|
4
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the current value of a dashboard state topic and follows later updates.
|
|
8
|
+
*
|
|
9
|
+
* The hook suspends until the topic publishes its first value, using the message bus query
|
|
10
|
+
* deadline. A topic declared with `TopicResult` resolves to its successful value; a
|
|
11
|
+
* failed result throws a `TopicError` to the nearest error boundary.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function ForegroundApplication() {
|
|
16
|
+
* const foregroundId = useTopic('applications.foreground')
|
|
17
|
+
* return <span>{foregroundId ?? 'No application in the foreground'}</span>
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
export const useTopic = createStateSourceHook({
|
|
24
|
+
getState: getTopicState,
|
|
25
|
+
// `getCurrent` throws a recorded read failure, which surfaces it from render like a thrown value.
|
|
26
|
+
shouldSuspend: (instance, topic: StateTopic) =>
|
|
27
|
+
getTopicState(instance, topic).getCurrent() === undefined,
|
|
28
|
+
suspender: resolveTopic,
|
|
29
|
+
}) as <K extends StateTopic>(topic: K) => TopicData<K>
|
|
@@ -121,11 +121,18 @@ describe('useUpdateFavorite', () => {
|
|
|
121
121
|
mockSetFavorite.mockRejectedValue(new Error('mutate failed'))
|
|
122
122
|
|
|
123
123
|
const {result} = renderHook(() => useUpdateFavorite(handle), {wrapper})
|
|
124
|
+
let error: unknown
|
|
124
125
|
|
|
125
126
|
await act(async () => {
|
|
126
|
-
|
|
127
|
+
try {
|
|
128
|
+
await result.current.favorite()
|
|
129
|
+
} catch (caughtError) {
|
|
130
|
+
error = caughtError
|
|
131
|
+
}
|
|
127
132
|
})
|
|
128
133
|
|
|
134
|
+
expect(error).toBeInstanceOf(Error)
|
|
135
|
+
expect((error as Error).message).toBe('mutate failed')
|
|
129
136
|
await waitFor(() => expect(result.current.error).toBeInstanceOf(Error))
|
|
130
137
|
})
|
|
131
138
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {type ActionsResult, type DocumentAction} from '@sanity/sdk'
|
|
2
|
-
import {type SanityDocument} from 'groq'
|
|
1
|
+
import {type ActionsResult, type DocumentAction, type ResolveDocument} from '@sanity/sdk'
|
|
3
2
|
|
|
4
3
|
import {type ResourceHandle} from '../../config/handles'
|
|
5
4
|
import {useApplyActions} from '../helpers/useApplyActions'
|
|
@@ -20,7 +19,7 @@ interface UseApplyDocumentActions {
|
|
|
20
19
|
| DocumentAction<TDocumentType, TDataset, TProjectId>
|
|
21
20
|
| DocumentAction<TDocumentType, TDataset, TProjectId>[],
|
|
22
21
|
options?: ResourceHandle,
|
|
23
|
-
) => Promise<ActionsResult<
|
|
22
|
+
) => Promise<ActionsResult<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
/**
|
|
@@ -199,7 +198,7 @@ interface UseApplyDocumentActions {
|
|
|
199
198
|
* perspective: {releaseName: 'summer-drop'},
|
|
200
199
|
* })
|
|
201
200
|
*
|
|
202
|
-
* apply(editDocument(docHandle, {title: 'Updated for release'}))
|
|
201
|
+
* apply(editDocument(docHandle, {set: {title: 'Updated for release'}}))
|
|
203
202
|
* }
|
|
204
203
|
*
|
|
205
204
|
* return <button onClick={handleEdit}>Edit in Release</button>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {createDocument} from '@sanity/sdk'
|
|
1
|
+
import {createDocument, type ResolveDocument} from '@sanity/sdk'
|
|
2
2
|
import {randomUuid} from '@sanity/sdk/_internal'
|
|
3
|
-
import {type SanityDocument} from 'groq'
|
|
4
3
|
|
|
5
4
|
import {type DocumentHandle, type DocumentTypeHandle} from '../../config/handles'
|
|
6
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -38,7 +37,7 @@ export function useCreateDocument<
|
|
|
38
37
|
options: DocumentTypeHandle<TDocumentType, TDataset, TProjectId>,
|
|
39
38
|
): (
|
|
40
39
|
initialValue?: Partial<
|
|
41
|
-
Omit<
|
|
40
|
+
Omit<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>, IgnoredKey>
|
|
42
41
|
>,
|
|
43
42
|
overrides?: CreateDocumentOverrides,
|
|
44
43
|
) => Promise<DocumentHandle<TDocumentType, TDataset, TProjectId>>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type DocumentOptions,
|
|
3
|
+
getDocumentState,
|
|
4
|
+
type JsonMatch,
|
|
5
|
+
type ResolveDocument,
|
|
6
|
+
resolveDocument,
|
|
7
|
+
} from '@sanity/sdk'
|
|
3
8
|
import {identity} from 'rxjs'
|
|
4
9
|
|
|
5
10
|
import {type DocumentHandle} from '../../config/handles'
|
|
@@ -47,7 +52,7 @@ interface UseDocument {
|
|
|
47
52
|
/** @internal */
|
|
48
53
|
<TDocumentType extends string, TDataset extends string, TProjectId extends string = string>(
|
|
49
54
|
options: UseDocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
|
|
50
|
-
): {data:
|
|
55
|
+
): {data: ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null}
|
|
51
56
|
|
|
52
57
|
/** @internal */
|
|
53
58
|
<
|
|
@@ -58,7 +63,7 @@ interface UseDocument {
|
|
|
58
63
|
>(
|
|
59
64
|
options: UseDocumentOptions<TPath, TDocumentType>,
|
|
60
65
|
): {
|
|
61
|
-
data: JsonMatch<
|
|
66
|
+
data: JsonMatch<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath> | undefined
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
/** @internal */
|
|
@@ -138,10 +143,10 @@ interface UseDocument {
|
|
|
138
143
|
): TPath extends string
|
|
139
144
|
? {
|
|
140
145
|
data:
|
|
141
|
-
| JsonMatch<
|
|
146
|
+
| JsonMatch<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath>
|
|
142
147
|
| undefined
|
|
143
148
|
}
|
|
144
|
-
: {data:
|
|
149
|
+
: {data: ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null}
|
|
145
150
|
|
|
146
151
|
/**
|
|
147
152
|
* @public
|
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
editDocument,
|
|
5
5
|
getDocumentState,
|
|
6
6
|
type JsonMatch,
|
|
7
|
+
type ResolveDocument,
|
|
7
8
|
resolveDocument,
|
|
8
9
|
} from '@sanity/sdk'
|
|
9
|
-
import {type SanityDocument} from 'groq'
|
|
10
10
|
import {useCallback} from 'react'
|
|
11
11
|
|
|
12
12
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -34,8 +34,8 @@ export function useEditDocument<
|
|
|
34
34
|
>(
|
|
35
35
|
options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
|
|
36
36
|
): (
|
|
37
|
-
nextValue: Updater<
|
|
38
|
-
) => Promise<ActionsResult<
|
|
37
|
+
nextValue: Updater<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>>,
|
|
38
|
+
) => Promise<ActionsResult<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
|
|
39
39
|
|
|
40
40
|
// Overload 2: Path provided, relies on Typegen
|
|
41
41
|
/**
|
|
@@ -54,8 +54,8 @@ export function useEditDocument<
|
|
|
54
54
|
>(
|
|
55
55
|
options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
|
|
56
56
|
): (
|
|
57
|
-
nextValue: Updater<JsonMatch<
|
|
58
|
-
) => Promise<ActionsResult<
|
|
57
|
+
nextValue: Updater<JsonMatch<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath>>,
|
|
58
|
+
) => Promise<ActionsResult<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
|
|
59
59
|
|
|
60
60
|
// Overload 3: Explicit type, no path
|
|
61
61
|
/**
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {getProjectionState, resolveProjection} from '@sanity/sdk'
|
|
2
|
-
import {type SanityProjectionResult} from 'groq'
|
|
1
|
+
import {getProjectionState, resolveProjection, type ResolveProjectionResult} from '@sanity/sdk'
|
|
3
2
|
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
4
3
|
import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
|
|
5
4
|
|
|
@@ -64,19 +63,19 @@ export interface useDocumentProjectionResults<TData> {
|
|
|
64
63
|
* @param options - Options including the document handle properties (`documentId`, `documentType`, etc.) and the `projection`.
|
|
65
64
|
* @returns The projected data, typed based on Typegen.
|
|
66
65
|
*
|
|
67
|
-
* @example Using Typegen for a book preview
|
|
66
|
+
* @example Using an existing experimental Typegen file for a book preview
|
|
68
67
|
* ```tsx
|
|
69
68
|
* // ProjectionComponent.tsx
|
|
70
|
-
* import {useDocumentProjection, type DocumentHandle} from '@sanity/sdk-react'
|
|
69
|
+
* import {defineProjection, useDocumentProjection, type DocumentHandle} from '@sanity/sdk-react'
|
|
71
70
|
* import {useRef} from 'react'
|
|
72
|
-
* import {defineProjection} from 'groq'
|
|
73
71
|
*
|
|
74
72
|
* // Define props using DocumentHandle with the specific document type
|
|
75
73
|
* type ProjectionComponentProps = {
|
|
76
74
|
* doc: DocumentHandle<'book'> // Typegen knows 'book'
|
|
77
75
|
* }
|
|
78
76
|
*
|
|
79
|
-
* //
|
|
77
|
+
* // Keep the projection string identical to its entry in the existing generated file.
|
|
78
|
+
* // Current Typegen does not scan defineProjection imported from the SDK.
|
|
80
79
|
* const myProjection = defineProjection(`{
|
|
81
80
|
* title,
|
|
82
81
|
* 'coverImage': cover.asset->url,
|
|
@@ -120,7 +119,7 @@ export function useDocumentProjection<
|
|
|
120
119
|
>(
|
|
121
120
|
options: useDocumentProjectionOptions<TProjection, TDocumentType, TDataset, TProjectId>,
|
|
122
121
|
): useDocumentProjectionResults<
|
|
123
|
-
|
|
122
|
+
ResolveProjectionResult<TProjection, TDocumentType, `${TProjectId}.${TDataset}`>
|
|
124
123
|
>
|
|
125
124
|
|
|
126
125
|
// Overload 2: Explicit type provided
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {getQueryState, type QueryOptions, resolveQuery} from '@sanity/sdk'
|
|
1
|
+
import {getQueryState, type QueryOptions, resolveQuery, type ResolveQueryResult} from '@sanity/sdk'
|
|
2
2
|
import {getQueryKey, parseQueryKey} from '@sanity/sdk/_internal'
|
|
3
|
-
import {type SanityQueryResult} from 'groq'
|
|
4
3
|
import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
|
|
5
4
|
|
|
6
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -83,7 +82,7 @@ export function useQuery<
|
|
|
83
82
|
options: UseQueryOptions<TQuery, TDataset, TProjectId>,
|
|
84
83
|
): {
|
|
85
84
|
/** The query result, typed based on the GROQ query string */
|
|
86
|
-
data:
|
|
85
|
+
data: ResolveQueryResult<TQuery, `${TProjectId}.${TDataset}`>
|
|
87
86
|
/** True if a query transition is in progress */
|
|
88
87
|
isPending: boolean
|
|
89
88
|
}
|
|
@@ -205,6 +204,6 @@ export function useQuery(options: WithResourceNameSupport<QueryOptions>): {
|
|
|
205
204
|
|
|
206
205
|
// Subscribe to updates and get the current data
|
|
207
206
|
// useSyncExternalStore ensures the component re-renders when the data changes
|
|
208
|
-
const data = useSyncExternalStore(subscribe, getCurrent) as
|
|
207
|
+
const data = useSyncExternalStore(subscribe, getCurrent) as ResolveQueryResult
|
|
209
208
|
return useMemo(() => ({data, isPending}), [data, isPending])
|
|
210
209
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useStudioWorkspacesByProjectIdDataset-DxUlukmF.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","from","Observable","of","catchError","switchMap","OS_BUS_KEY","Symbol","for","isDashboardEnvironment","globalThis","observeDashboardToken","undefined","pipe","os","subscribe","refreshDashboardToken","then","emit","ClientError","AuthStateType","setAuthToken","React","PropsWithChildren","useEffect","useRef","useAuthState","useSanityInstance","isDashboardEnvironment","observeDashboardToken","refreshDashboardToken","DashboardTokenRefresh","t0","$","_c","children","instance","authState","processed401ErrorRef","t1","t2","token$","subscription","subscribe","token","unsubscribe","t3","error","type","has401Error","ERROR","statusCode","current","t4","DashboardTokenRefreshProvider","FC","getDashboardOrganizationId","useMemo","useSyncExternalStore","useSanityInstance","useOrganizationId","$","_c","instance","t0","subscribe","getCurrent","SDK_CHANNEL_NAME","SDK_NODE_NAME","useEffect","useState","useWindowConnection","DashboardResource","id","name","title","basePath","projectId","dataset","type","userApplicationId","url","WorkspacesByProjectIdDataset","key","StudioWorkspacesResult","workspacesByProjectIdAndDataset","error","useStudioWorkspacesByProjectIdDataset","$","_c","t0","Symbol","for","setWorkspacesByProjectIdAndDataset","setError","t1","connectTo","fetch","t2","t3","fetchWorkspaces","signal","data","undefined","workspaceMap","noProjectIdAndDataset","context","availableResources","forEach","resource","push","const","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/context/dashboardToken.ts","../src/context/DashboardTokenRefresh.tsx","../src/hooks/dashboard/useOrganizationId.tsx","../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 {from, type Observable, of} from 'rxjs'\nimport {catchError, switchMap} from 'rxjs/operators'\n\n// The dashboard host installs its shared message bus on this well-known global\n// symbol before it loads the apps it embeds in its own window. It must match\n// the key used by `@sanity/workbench` (`Symbol.for('sanity.os.bus')`).\nconst OS_BUS_KEY = Symbol.for('sanity.os.bus')\n\n/**\n * Whether this app is running inside the dashboard, embedded in its window.\n *\n * Apps embedded this way share the dashboard's realm, so the bus it installs\n * is visible on `globalThis`. This is `false` in a standalone app, where we\n * must never import `@sanity/workbench` (it would install a bus and add bundle\n * weight for no reason). Note: this is a different embedding model to the Core\n * UI iframe, which is detected separately via the dashboard context — that\n * signal is not set for apps sharing the dashboard's window.\n *\n * @internal\n */\nexport function isDashboardEnvironment(): boolean {\n return typeof globalThis === 'object' && OS_BUS_KEY in globalThis\n}\n\n/**\n * Observes the session token issued by the dashboard \"OS\", tracking the OS auth\n * state over time.\n *\n * Returns `undefined` when the app is not embedded in the dashboard, so the\n * caller uses its normal auth flow. Inside the dashboard, subscribes to the\n * `auth.token` state topic, emitting the current token — or `null` when the OS\n * is signed out — and re-emitting as the OS auth state changes, so sign-in/out\n * propagates instead of being captured once. Any bus error is treated as \"no\n * token\" (`null`). The token is used in-memory only and never persisted.\n *\n * @internal\n */\nexport function observeDashboardToken(): Observable<string | null> | undefined {\n if (!isDashboardEnvironment()) return undefined\n\n return from(import('@sanity/workbench')).pipe(\n switchMap(({os}) => os.subscribe('auth.token')),\n // Any failure (importing the host bundle, or the subscription) means \"no OS token\".\n catchError(() => of(null)),\n )\n}\n\n/**\n * Asks the dashboard \"OS\" to reissue the session token, e.g. after its current\n * one was rejected with a 401. Fire-and-forget: the reissued token arrives via\n * the `auth.token` subscription in {@link observeDashboardToken}. No-op outside\n * the dashboard.\n *\n * @internal\n */\nexport function refreshDashboardToken(): void {\n if (!isDashboardEnvironment()) return\n\n void import('@sanity/workbench').then(\n ({os}) => os.emit('auth.token.refresh', undefined),\n () => {},\n )\n}\n","import {type ClientError} from '@sanity/client'\nimport {AuthStateType, setAuthToken} from '@sanity/sdk'\nimport React, {type PropsWithChildren, useEffect, useRef} from 'react'\n\nimport {useAuthState} from '../hooks/auth/useAuthState'\nimport {useSanityInstance} from '../hooks/context/useSanityInstance'\nimport {\n isDashboardEnvironment,\n observeDashboardToken,\n refreshDashboardToken,\n} from './dashboardToken'\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({children}: PropsWithChildren) {\n const instance = useSanityInstance()\n const authState = useAuthState()\n const processed401ErrorRef = useRef<unknown | null>(null)\n\n useEffect(() => {\n const token$ = observeDashboardToken()\n if (!token$) return undefined\n const subscription = token$.subscribe((token) => setAuthToken(instance, token))\n return () => subscription.unsubscribe()\n }, [instance])\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 refreshDashboardToken()\n } else if (!has401Error) {\n processed401ErrorRef.current = null\n }\n }, [authState])\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 if (isDashboardEnvironment()) {\n return <DashboardTokenRefresh>{children}</DashboardTokenRefresh>\n }\n\n return children\n}\n","import {getDashboardOrganizationId} from '@sanity/sdk'\nimport {useMemo, useSyncExternalStore} from 'react'\n\nimport {useSanityInstance} from '../context/useSanityInstance'\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 * @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(() => getDashboardOrganizationId(instance), [instance])\n\n return useSyncExternalStore(subscribe, getCurrent)\n}\n","import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'\nimport {useEffect, useState} from 'react'\n\nimport {useWindowConnection} from '../comlink/useWindowConnection'\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\n/**\n * Hook that fetches studio workspaces and organizes them by projectId:dataset\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 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":"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"}
|