@sanity/sdk-react 3.0.0 → 3.2.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 +498 -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 +90 -87
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -115
- package/dist/index.js.map +1 -1
- package/dist/useStudioWorkspacesByProjectIdDataset-BZ_Ud887.js +314 -0
- 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 +71 -0
- package/src/_exports/index.ts +1 -1
- package/src/components/auth/AuthBoundary.test.tsx +34 -0
- package/src/components/auth/AuthBoundary.tsx +1 -2
- package/src/context/DashboardTokenRefresh.test.tsx +124 -22
- package/src/context/DashboardTokenRefresh.tsx +70 -20
- 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/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 +4 -4
- 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/src/utils/resolveOrgResources.test.ts +2 -2
- package/src/utils/resolveOrgResources.ts +2 -2
- package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js +0 -177
- package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js.map +0 -1
- package/src/context/dashboardToken.ts +0 -63
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {requireDashboardMessageBus} from '@sanity/sdk/_internal'
|
|
2
|
+
import {
|
|
3
|
+
type EventTopic,
|
|
4
|
+
type MessageBusEmitOptions,
|
|
5
|
+
type MessageBusEmitResult,
|
|
6
|
+
type PayloadOf,
|
|
7
|
+
type ReplyOf,
|
|
8
|
+
} from '@sanity/sdk/dashboard'
|
|
9
|
+
import {useCallback} from 'react'
|
|
10
|
+
|
|
11
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Emits a dashboard event topic, typed to the topic's payload and reply.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export type TopicEmitter<K extends EventTopic> = (
|
|
18
|
+
...args: PayloadOf<K> extends void
|
|
19
|
+
? [payload?: void, options?: MessageBusEmitOptions]
|
|
20
|
+
: [payload: PayloadOf<K>, options?: MessageBusEmitOptions]
|
|
21
|
+
) => MessageBusEmitResult<ReplyOf<K>>
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns a stable function that emits a dashboard event topic.
|
|
25
|
+
*
|
|
26
|
+
* The event is sent immediately. Ignore the lazy result for fire-and-forget delivery, await it
|
|
27
|
+
* inside `useTransition` to track a reply, or read it with React `use` to suspend. A reply
|
|
28
|
+
* that fails (`NO_RESPONDER`, `TIMEOUT`, `ABORTED`) rejects the result; when read with `use`
|
|
29
|
+
* that reaches the nearest error boundary, so pair the `Suspense` with one.
|
|
30
|
+
*
|
|
31
|
+
* @example Fire and forget
|
|
32
|
+
* ```tsx
|
|
33
|
+
* function ExpandPanel() {
|
|
34
|
+
* const setPanelMode = useEmit('panels.mode.set')
|
|
35
|
+
* return (
|
|
36
|
+
* <button onClick={() => setPanelMode({name: 'favorites', mode: 'full'})}>
|
|
37
|
+
* Expand
|
|
38
|
+
* </button>
|
|
39
|
+
* )
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example Await a reply with Suspense
|
|
44
|
+
* ```tsx
|
|
45
|
+
* function Session({request}: {request: MessageBusEmitResult<string>}) {
|
|
46
|
+
* use(request)
|
|
47
|
+
* return <p>Ready</p>
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* function SessionAccordion() {
|
|
51
|
+
* const refreshToken = useEmit('auth.token.refresh')
|
|
52
|
+
* const [request, setRequest] = useState<MessageBusEmitResult<string> | null>(null)
|
|
53
|
+
*
|
|
54
|
+
* return (
|
|
55
|
+
* <details
|
|
56
|
+
* onToggle={(event) => setRequest(event.currentTarget.open ? refreshToken() : null)}
|
|
57
|
+
* >
|
|
58
|
+
* <summary>Session</summary>
|
|
59
|
+
* <ErrorBoundary fallback={<p>Could not refresh</p>}>
|
|
60
|
+
* <Suspense fallback={<p>Refreshing...</p>}>
|
|
61
|
+
* {request && <Session request={request} />}
|
|
62
|
+
* </Suspense>
|
|
63
|
+
* </ErrorBoundary>
|
|
64
|
+
* </details>
|
|
65
|
+
* )
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
export function useEmit<K extends EventTopic>(topic: K): TopicEmitter<K> {
|
|
72
|
+
const instance = useSanityInstance()
|
|
73
|
+
const messageBus = requireDashboardMessageBus(instance, `emit topic "${topic}"`)
|
|
74
|
+
return useCallback<TopicEmitter<K>>(
|
|
75
|
+
(...args) => messageBus.emit(topic, ...args),
|
|
76
|
+
[messageBus, topic],
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -45,9 +45,7 @@ import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
|
45
45
|
* }
|
|
46
46
|
* ```
|
|
47
47
|
*/
|
|
48
|
-
export function useNavigate(
|
|
49
|
-
navigateFn: (options: PathChangeMessage['data']) => void,
|
|
50
|
-
): void {
|
|
48
|
+
export function useNavigate(navigateFn: (options: PathChangeMessage['data']) => void): void {
|
|
51
49
|
useWindowConnection<PathChangeMessage, never>({
|
|
52
50
|
name: SDK_NODE_NAME,
|
|
53
51
|
connectTo: SDK_CHANNEL_NAME,
|
|
@@ -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,5 +1,5 @@
|
|
|
1
|
-
import {createDocument} from '@sanity/sdk'
|
|
2
|
-
import {
|
|
1
|
+
import {createDocument, type ResolveDocument} from '@sanity/sdk'
|
|
2
|
+
import {randomUuid} from '@sanity/sdk/_internal'
|
|
3
3
|
|
|
4
4
|
import {type DocumentHandle, type DocumentTypeHandle} from '../../config/handles'
|
|
5
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -37,7 +37,7 @@ export function useCreateDocument<
|
|
|
37
37
|
options: DocumentTypeHandle<TDocumentType, TDataset, TProjectId>,
|
|
38
38
|
): (
|
|
39
39
|
initialValue?: Partial<
|
|
40
|
-
Omit<
|
|
40
|
+
Omit<ResolveDocument<TDocumentType, `${TProjectId}.${TDataset}`>, IgnoredKey>
|
|
41
41
|
>,
|
|
42
42
|
overrides?: CreateDocumentOverrides,
|
|
43
43
|
) => Promise<DocumentHandle<TDocumentType, TDataset, TProjectId>>
|
|
@@ -109,7 +109,7 @@ export function useCreateDocument(
|
|
|
109
109
|
const apply = useApplyDocumentActions()
|
|
110
110
|
|
|
111
111
|
return async (initialValue, overrides) => {
|
|
112
|
-
const documentId = overrides?.documentId ?? options.documentId ??
|
|
112
|
+
const documentId = overrides?.documentId ?? options.documentId ?? randomUuid()
|
|
113
113
|
const handle: DocumentHandle = {...options, documentId}
|
|
114
114
|
await apply(createDocument(handle, initialValue))
|
|
115
115
|
return handle
|
|
@@ -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
|
}
|
|
@@ -52,10 +52,10 @@ describe('resolveOrgResources', () => {
|
|
|
52
52
|
await resolveOrgResources(mockInstance, ORG_ID)
|
|
53
53
|
|
|
54
54
|
expect(mockRequest).toHaveBeenCalledWith(
|
|
55
|
-
expect.objectContaining({
|
|
55
|
+
expect.objectContaining({url: '/media-libraries', query: {organizationId: ORG_ID}}),
|
|
56
56
|
)
|
|
57
57
|
expect(mockRequest).toHaveBeenCalledWith(
|
|
58
|
-
expect.objectContaining({
|
|
58
|
+
expect.objectContaining({url: '/canvases', query: {organizationId: ORG_ID}}),
|
|
59
59
|
)
|
|
60
60
|
})
|
|
61
61
|
|
|
@@ -48,12 +48,12 @@ export async function resolveOrgResources(
|
|
|
48
48
|
|
|
49
49
|
const [mediaLibrariesResult, canvasesResult] = await Promise.allSettled([
|
|
50
50
|
client.request<OrgResourcesApiResponse>({
|
|
51
|
-
|
|
51
|
+
url: `/media-libraries`,
|
|
52
52
|
query: {organizationId},
|
|
53
53
|
tag: 'org-resources.media-libraries',
|
|
54
54
|
}),
|
|
55
55
|
client.request<OrgResourcesApiResponse>({
|
|
56
|
-
|
|
56
|
+
url: `/canvases`,
|
|
57
57
|
query: {organizationId},
|
|
58
58
|
tag: 'org-resources.canvases',
|
|
59
59
|
}),
|