@sanity/sdk-react 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +92 -26
- package/dist/index.js +304 -193
- package/dist/index.js.map +1 -1
- package/package.json +9 -11
- package/src/_exports/sdk-react.ts +4 -0
- package/src/components/SDKProvider.tsx +36 -8
- package/src/components/SanityApp.tsx +2 -2
- package/src/components/auth/AuthBoundary.tsx +8 -1
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +100 -56
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/context/ResourceProvider.tsx +5 -4
- package/src/context/ResourcesContext.tsx +7 -0
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/dashboard/useDispatchIntent.test.ts +6 -6
- package/src/hooks/dashboard/useDispatchIntent.ts +6 -6
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -13
- package/src/hooks/document/useApplyDocumentActions.test.ts +10 -10
- package/src/hooks/document/useApplyDocumentActions.ts +17 -17
- package/src/hooks/document/useDocument.ts +5 -5
- package/src/hooks/document/useDocumentEvent.ts +4 -4
- package/src/hooks/document/useDocumentPermissions.test.tsx +10 -10
- package/src/hooks/document/useDocumentPermissions.ts +8 -8
- package/src/hooks/document/useDocumentSyncStatus.ts +2 -2
- package/src/hooks/document/useEditDocument.ts +2 -2
- package/src/hooks/documents/useDocuments.ts +9 -6
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +131 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +2 -2
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +9 -8
- package/src/hooks/presence/usePresence.test.tsx +56 -9
- package/src/hooks/presence/usePresence.ts +23 -4
- package/src/hooks/preview/useDocumentPreview.tsx +8 -7
- package/src/hooks/projection/useDocumentProjection.ts +6 -6
- package/src/hooks/query/useQuery.ts +10 -9
- package/src/hooks/releases/useActiveReleases.ts +10 -10
- package/src/hooks/releases/usePerspective.ts +9 -9
- package/src/context/SourcesContext.tsx +0 -7
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -107
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {render, screen} from '../../../test/test-utils'
|
|
4
|
+
import {ChunkLoadError} from './ChunkLoadError'
|
|
5
|
+
import {CHUNK_RELOAD_STORAGE_KEY} from './chunkReloadStorage'
|
|
6
|
+
|
|
7
|
+
const noop = (): void => {}
|
|
8
|
+
|
|
9
|
+
describe('ChunkLoadError', () => {
|
|
10
|
+
const reloadSpy = vi.fn()
|
|
11
|
+
const originalLocation = window.location
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
reloadSpy.mockReset()
|
|
15
|
+
window.sessionStorage.clear()
|
|
16
|
+
Object.defineProperty(window, 'location', {
|
|
17
|
+
configurable: true,
|
|
18
|
+
value: {...originalLocation, reload: reloadSpy},
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
Object.defineProperty(window, 'location', {configurable: true, value: originalLocation})
|
|
24
|
+
window.sessionStorage.clear()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('triggers an automatic reload and renders nothing on the first occurrence', () => {
|
|
28
|
+
render(
|
|
29
|
+
<ChunkLoadError
|
|
30
|
+
error={new Error('Failed to fetch dynamically imported module')}
|
|
31
|
+
resetErrorBoundary={noop}
|
|
32
|
+
/>,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(reloadSpy).toHaveBeenCalledTimes(1)
|
|
36
|
+
expect(window.sessionStorage.getItem(CHUNK_RELOAD_STORAGE_KEY)).toBe('1')
|
|
37
|
+
expect(screen.queryByText(/new version/i)).toBeNull()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('renders the manual reload UI when the flag is already set', () => {
|
|
41
|
+
window.sessionStorage.setItem(CHUNK_RELOAD_STORAGE_KEY, '1')
|
|
42
|
+
|
|
43
|
+
render(
|
|
44
|
+
<ChunkLoadError
|
|
45
|
+
error={new Error('Failed to fetch dynamically imported module')}
|
|
46
|
+
resetErrorBoundary={noop}
|
|
47
|
+
/>,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
expect(reloadSpy).not.toHaveBeenCalled()
|
|
51
|
+
expect(screen.getByText('A new version is available')).toBeInTheDocument()
|
|
52
|
+
|
|
53
|
+
const button = screen.getByRole('button', {name: 'Reload page'})
|
|
54
|
+
button.click()
|
|
55
|
+
|
|
56
|
+
expect(reloadSpy).toHaveBeenCalledTimes(1)
|
|
57
|
+
expect(window.sessionStorage.getItem(CHUNK_RELOAD_STORAGE_KEY)).toBeNull()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {useEffect} from 'react'
|
|
2
|
+
import {type FallbackProps} from 'react-error-boundary'
|
|
3
|
+
|
|
4
|
+
import {clearChunkReloadFlag, readChunkReloadFlag, setChunkReloadFlag} from './chunkReloadStorage'
|
|
5
|
+
import {Error} from './Error'
|
|
6
|
+
|
|
7
|
+
function reload(): void {
|
|
8
|
+
try {
|
|
9
|
+
window.location.reload()
|
|
10
|
+
} catch {
|
|
11
|
+
// No-op: nothing useful we can do if reload itself throws.
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default fallback rendered when a dynamic-import or chunk-load error
|
|
17
|
+
* bubbles up to the SDK's top-level error boundary.
|
|
18
|
+
*
|
|
19
|
+
* On the first occurrence in a session we set a flag and trigger
|
|
20
|
+
* window.location.reload(), since chunk-load errors almost always indicate a
|
|
21
|
+
* stale tab that simply needs a fresh index.html. If the flag is already set
|
|
22
|
+
* we render a manual reload UI instead, which prevents an infinite reload
|
|
23
|
+
* loop in the rare case the error is genuinely unrecoverable (network
|
|
24
|
+
* outage, CSP, etc.).
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
export function ChunkLoadError(_props: FallbackProps): React.ReactNode {
|
|
29
|
+
const alreadyAttempted = readChunkReloadFlag()
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (alreadyAttempted) return
|
|
33
|
+
setChunkReloadFlag()
|
|
34
|
+
reload()
|
|
35
|
+
}, [alreadyAttempted])
|
|
36
|
+
|
|
37
|
+
if (!alreadyAttempted) {
|
|
38
|
+
// Render nothing during the brief window before the page reloads so the
|
|
39
|
+
// user does not see a flash of error UI.
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Error
|
|
45
|
+
heading="A new version is available"
|
|
46
|
+
description="The page tried to load an asset that no longer exists. Reload to continue with the latest version."
|
|
47
|
+
cta={{
|
|
48
|
+
text: 'Reload page',
|
|
49
|
+
onClick: () => {
|
|
50
|
+
clearChunkReloadFlag()
|
|
51
|
+
reload()
|
|
52
|
+
},
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-storage key tracking whether the SDK has already attempted an
|
|
3
|
+
* automatic reload in response to a chunk-load error during this session.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export const CHUNK_RELOAD_STORAGE_KEY = '__sanity_sdk_chunk_reload_attempted'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true when this session has already triggered an automatic reload.
|
|
11
|
+
* Returns false if session storage is unreadable.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
export function readChunkReloadFlag(): boolean {
|
|
16
|
+
try {
|
|
17
|
+
if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
20
|
+
return window.sessionStorage.getItem(CHUNK_RELOAD_STORAGE_KEY) !== null
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Marks the session as having attempted an automatic reload, so the next
|
|
28
|
+
* chunk-load error renders the manual reload UI instead of looping.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
export function setChunkReloadFlag(): void {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') return
|
|
35
|
+
window.sessionStorage.setItem(CHUNK_RELOAD_STORAGE_KEY, '1')
|
|
36
|
+
} catch {
|
|
37
|
+
// Storage may be unavailable (private mode quotas, disabled cookies).
|
|
38
|
+
// Falling through means the user sees the manual-reload UI instead of an
|
|
39
|
+
// automatic reload, which is the correct degradation.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Clears the chunk-reload flag. Called from SDKProvider once the SDK
|
|
45
|
+
* mounts successfully past the error boundary so a future incident in the
|
|
46
|
+
* same session can trigger another automatic reload.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export function clearChunkReloadFlag(): void {
|
|
51
|
+
try {
|
|
52
|
+
if (typeof window === 'undefined' || typeof window.sessionStorage === 'undefined') return
|
|
53
|
+
window.sessionStorage.removeItem(CHUNK_RELOAD_STORAGE_KEY)
|
|
54
|
+
} catch {
|
|
55
|
+
// No-op: see setChunkReloadFlag.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {initTelemetry} from '@sanity/sdk/_internal'
|
|
3
|
-
import {
|
|
3
|
+
import {useContext, useEffect, useMemo, useRef} from 'react'
|
|
4
4
|
|
|
5
5
|
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
6
|
+
import {SanityInstanceProvider} from './SanityInstanceProvider'
|
|
6
7
|
|
|
7
8
|
const DEFAULT_FALLBACK = (
|
|
8
9
|
<>
|
|
@@ -110,8 +111,8 @@ export function ResourceProvider({
|
|
|
110
111
|
}, [instance])
|
|
111
112
|
|
|
112
113
|
return (
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
</
|
|
114
|
+
<SanityInstanceProvider instance={instance} fallback={fallback ?? DEFAULT_FALLBACK}>
|
|
115
|
+
{children}
|
|
116
|
+
</SanityInstanceProvider>
|
|
116
117
|
)
|
|
117
118
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {act, render, screen} from '@testing-library/react'
|
|
3
|
+
import {use, useEffect} from 'react'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
7
|
+
import {SanityInstanceProvider} from './SanityInstanceProvider'
|
|
8
|
+
|
|
9
|
+
function promiseWithResolvers<T = void>(): {
|
|
10
|
+
promise: Promise<T>
|
|
11
|
+
resolve: (t: T) => void
|
|
12
|
+
reject: (error: unknown) => void
|
|
13
|
+
} {
|
|
14
|
+
let resolve!: (t: T) => void
|
|
15
|
+
let reject!: (error: unknown) => void
|
|
16
|
+
const promise = new Promise<T>((res, rej) => {
|
|
17
|
+
resolve = res
|
|
18
|
+
reject = rej
|
|
19
|
+
})
|
|
20
|
+
return {resolve, reject, promise}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('SanityInstanceProvider', () => {
|
|
24
|
+
it('renders children', () => {
|
|
25
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
26
|
+
|
|
27
|
+
render(
|
|
28
|
+
<SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
|
|
29
|
+
<div data-testid="test-child">Child Component</div>
|
|
30
|
+
</SanityInstanceProvider>,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
expect(screen.getByTestId('test-child')).toBeInTheDocument()
|
|
34
|
+
instance.dispose()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('provides the given instance via context', async () => {
|
|
38
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
39
|
+
const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
|
|
40
|
+
|
|
41
|
+
const CaptureInstance = () => {
|
|
42
|
+
const ctx = use(SanityInstanceContext)
|
|
43
|
+
useEffect(() => resolve(ctx), [ctx])
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<SanityInstanceProvider instance={instance} fallback={null}>
|
|
49
|
+
<CaptureInstance />
|
|
50
|
+
</SanityInstanceProvider>,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const provided = await promise
|
|
54
|
+
expect(provided).toBe(instance)
|
|
55
|
+
instance.dispose()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('shows fallback during suspense', async () => {
|
|
59
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
60
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
61
|
+
const {promise, resolve} = promiseWithResolvers()
|
|
62
|
+
|
|
63
|
+
function SuspendingChild(): React.ReactNode {
|
|
64
|
+
throw promise
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(
|
|
68
|
+
<SanityInstanceProvider
|
|
69
|
+
instance={instance}
|
|
70
|
+
fallback={<div data-testid="fallback">Loading...</div>}
|
|
71
|
+
>
|
|
72
|
+
<SuspendingChild />
|
|
73
|
+
</SanityInstanceProvider>,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
expect(screen.getByTestId('fallback')).toBeInTheDocument()
|
|
77
|
+
act(() => {
|
|
78
|
+
resolve()
|
|
79
|
+
})
|
|
80
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
81
|
+
instance.dispose()
|
|
82
|
+
consoleSpy.mockRestore()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('does not dispose the instance on unmount', async () => {
|
|
86
|
+
const instance = createSanityInstance({projectId: 'test', dataset: 'test'})
|
|
87
|
+
|
|
88
|
+
const {unmount} = render(
|
|
89
|
+
<SanityInstanceProvider instance={instance} fallback={null}>
|
|
90
|
+
<div />
|
|
91
|
+
</SanityInstanceProvider>,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
unmount()
|
|
95
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
96
|
+
|
|
97
|
+
expect(instance.isDisposed()).toBe(false)
|
|
98
|
+
instance.dispose()
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {Suspense} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Props for the SanityInstanceProvider component
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export interface SanityInstanceProviderProps {
|
|
11
|
+
/**
|
|
12
|
+
* A pre-created SanityInstance to provide to child components.
|
|
13
|
+
* The caller owns the instance lifecycle — SanityInstanceProvider
|
|
14
|
+
* will not dispose it on unmount.
|
|
15
|
+
*/
|
|
16
|
+
instance: SanityInstance
|
|
17
|
+
/**
|
|
18
|
+
* React node to show while content is loading.
|
|
19
|
+
* Used as the fallback for the internal Suspense boundary.
|
|
20
|
+
*/
|
|
21
|
+
fallback: React.ReactNode
|
|
22
|
+
children: React.ReactNode
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provides an externally-created Sanity instance to child components through React Context.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* Unlike {@link ResourceProvider}, this component does not create or dispose a SanityInstance.
|
|
32
|
+
* The caller is responsible for creating the instance via `createSanityInstance` and disposing
|
|
33
|
+
* it when appropriate. This is useful when a non-React system layer (e.g. a state machine)
|
|
34
|
+
* owns the instance and the React tree should consume it without managing its lifecycle.
|
|
35
|
+
*
|
|
36
|
+
* All SDK hooks (`useSanityInstance`, `useDocuments`, etc.) will read from the provided instance.
|
|
37
|
+
*
|
|
38
|
+
* @example Providing a pre-created instance
|
|
39
|
+
* ```tsx
|
|
40
|
+
* import { createSanityInstance, type SanityConfig } from '@sanity/sdk'
|
|
41
|
+
* import { SanityInstanceProvider } from '@sanity/sdk-react'
|
|
42
|
+
*
|
|
43
|
+
* const config: SanityConfig = {
|
|
44
|
+
* projectId: 'my-project-id',
|
|
45
|
+
* dataset: 'production',
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* const instance = createSanityInstance(config)
|
|
49
|
+
*
|
|
50
|
+
* function App() {
|
|
51
|
+
* return (
|
|
52
|
+
* <SanityInstanceProvider instance={instance} fallback={<div>Loading...</div>}>
|
|
53
|
+
* <MyApp />
|
|
54
|
+
* </SanityInstanceProvider>
|
|
55
|
+
* )
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @category Components
|
|
60
|
+
*/
|
|
61
|
+
export function SanityInstanceProvider({
|
|
62
|
+
instance,
|
|
63
|
+
fallback,
|
|
64
|
+
children,
|
|
65
|
+
}: SanityInstanceProviderProps): React.ReactNode {
|
|
66
|
+
return (
|
|
67
|
+
<SanityInstanceContext.Provider value={instance}>
|
|
68
|
+
<Suspense fallback={fallback}>{children}</Suspense>
|
|
69
|
+
</SanityInstanceContext.Provider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -27,13 +27,20 @@ export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): s
|
|
|
27
27
|
const instance = useSanityInstance()
|
|
28
28
|
const [error, setError] = useState<string | null>(null)
|
|
29
29
|
|
|
30
|
+
const isInactive = disabled || !projectIds || projectIds.length === 0
|
|
31
|
+
|
|
32
|
+
// Reset stale errors when verification turns off so the next activation
|
|
33
|
+
// doesn't briefly leak the previous result.
|
|
34
|
+
const [prevInactive, setPrevInactive] = useState(isInactive)
|
|
35
|
+
if (prevInactive !== isInactive) {
|
|
36
|
+
setPrevInactive(isInactive)
|
|
37
|
+
if (isInactive) setError(null)
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
useEffect(() => {
|
|
31
|
-
if (
|
|
32
|
-
if (error !== null) setError(null)
|
|
33
|
-
return
|
|
34
|
-
}
|
|
41
|
+
if (isInactive) return
|
|
35
42
|
|
|
36
|
-
const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds)
|
|
43
|
+
const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds!)
|
|
37
44
|
|
|
38
45
|
const subscription = verificationObservable$.subscribe((result: OrgVerificationResult) => {
|
|
39
46
|
setError(result.error)
|
|
@@ -42,7 +49,7 @@ export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): s
|
|
|
42
49
|
return () => {
|
|
43
50
|
subscription.unsubscribe()
|
|
44
51
|
}
|
|
45
|
-
}, [instance,
|
|
52
|
+
}, [instance, isInactive, projectIds])
|
|
46
53
|
|
|
47
54
|
return error
|
|
48
55
|
}
|
|
@@ -167,11 +167,11 @@ describe('useDispatchIntent', () => {
|
|
|
167
167
|
})
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
-
it('should send intent message with media library
|
|
170
|
+
it('should send intent message with media library resource', () => {
|
|
171
171
|
const mockMediaLibraryHandle = {
|
|
172
172
|
documentId: 'test-asset-id',
|
|
173
173
|
documentType: 'sanity.asset',
|
|
174
|
-
|
|
174
|
+
resourceName: 'media-library',
|
|
175
175
|
} as const
|
|
176
176
|
|
|
177
177
|
const {result} = renderHook(() =>
|
|
@@ -196,11 +196,11 @@ describe('useDispatchIntent', () => {
|
|
|
196
196
|
})
|
|
197
197
|
})
|
|
198
198
|
|
|
199
|
-
it('should send intent message with canvas
|
|
199
|
+
it('should send intent message with canvas resource', () => {
|
|
200
200
|
const mockCanvasHandle = {
|
|
201
201
|
documentId: 'test-canvas-document-id',
|
|
202
202
|
documentType: 'sanity.canvas.document',
|
|
203
|
-
|
|
203
|
+
resourceName: 'canvas',
|
|
204
204
|
} as const
|
|
205
205
|
|
|
206
206
|
const {result} = renderHook(() =>
|
|
@@ -226,7 +226,7 @@ describe('useDispatchIntent', () => {
|
|
|
226
226
|
})
|
|
227
227
|
|
|
228
228
|
describe('error handling', () => {
|
|
229
|
-
it('should throw error when neither
|
|
229
|
+
it('should throw error when neither resource nor projectId/dataset is provided', () => {
|
|
230
230
|
const invalidHandle = {
|
|
231
231
|
documentId: 'test-document-id',
|
|
232
232
|
documentType: 'test-document-type',
|
|
@@ -240,7 +240,7 @@ describe('useDispatchIntent', () => {
|
|
|
240
240
|
)
|
|
241
241
|
|
|
242
242
|
expect(() => result.current.dispatchIntent()).toThrow(
|
|
243
|
-
'useDispatchIntent: Unable to determine resource. Either `
|
|
243
|
+
'useDispatchIntent: Unable to determine resource. Either `resource`, `resourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
|
|
244
244
|
)
|
|
245
245
|
})
|
|
246
246
|
})
|
|
@@ -3,7 +3,7 @@ import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
|
3
3
|
import {useCallback} from 'react'
|
|
4
4
|
|
|
5
5
|
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
6
|
-
import {type
|
|
6
|
+
import {type WithResourceNameSupport} from '../helpers/useNormalizedResourceOptions'
|
|
7
7
|
import {useResourceIdFromDocumentHandle} from './utils/useResourceIdFromDocumentHandle'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -42,7 +42,7 @@ interface DispatchIntent {
|
|
|
42
42
|
interface UseDispatchIntentParams {
|
|
43
43
|
action?: 'edit'
|
|
44
44
|
intentId?: string
|
|
45
|
-
documentHandle:
|
|
45
|
+
documentHandle: WithResourceNameSupport<DocumentHandle>
|
|
46
46
|
parameters?: Record<string, unknown>
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -56,8 +56,8 @@ interface UseDispatchIntentParams {
|
|
|
56
56
|
* - `action` - Action to perform (currently only 'edit' is supported). Will prompt a picker if multiple handlers are available.
|
|
57
57
|
* - `intentId` - Specific ID of the intent to dispatch. Either `action` or `intentId` is required.
|
|
58
58
|
* - `documentHandle` - The document handle containing document ID, type, and either:
|
|
59
|
-
* - `projectId` and `dataset` for traditional dataset
|
|
60
|
-
* - `
|
|
59
|
+
* - `projectId` and `dataset` for traditional dataset resources, like `{documentId: '123', documentType: 'book', projectId: 'abc123', dataset: 'production'}`
|
|
60
|
+
* - `resource` for media library, canvas, or dataset resources, like `{documentId: '123', documentType: 'sanity.asset', resource: mediaLibrarySource('ml123')}` or `{documentId: '123', documentType: 'sanity.canvas.document', resource: canvasSource('canvas123')}`
|
|
61
61
|
* - `paremeters` - Optional parameters to include in the dispatch; will be passed to the resolved intent handler
|
|
62
62
|
* @returns An object containing:
|
|
63
63
|
* - `dispatchIntent` - Function to dispatch the intent message
|
|
@@ -119,10 +119,10 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
|
|
|
119
119
|
)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// Validate that we have a resource ID (which is computed from
|
|
122
|
+
// Validate that we have a resource ID (which is computed from resource/resourceName or projectId+dataset)
|
|
123
123
|
if (!resource.id) {
|
|
124
124
|
throw new Error(
|
|
125
|
-
'useDispatchIntent: Unable to determine resource. Either `
|
|
125
|
+
'useDispatchIntent: Unable to determine resource. Either `resource`, `resourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
|
|
126
126
|
)
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -23,11 +23,11 @@ describe('getResourceIdFromDocumentHandle', () => {
|
|
|
23
23
|
})
|
|
24
24
|
|
|
25
25
|
describe('with DocumentHandleWithSource - media library', () => {
|
|
26
|
-
it('should return media library ID and resourceType when media library
|
|
26
|
+
it('should return media library ID and resourceType when media library resource is provided', () => {
|
|
27
27
|
const documentHandle = {
|
|
28
28
|
documentId: 'test-asset-id',
|
|
29
29
|
documentType: 'sanity.asset',
|
|
30
|
-
|
|
30
|
+
resourceName: 'media-library',
|
|
31
31
|
} as const
|
|
32
32
|
|
|
33
33
|
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
@@ -38,13 +38,13 @@ describe('getResourceIdFromDocumentHandle', () => {
|
|
|
38
38
|
})
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it('should prioritize
|
|
41
|
+
it('should prioritize resource over projectId/dataset when both are provided', () => {
|
|
42
42
|
const documentHandle = {
|
|
43
43
|
documentId: 'test-asset-id',
|
|
44
44
|
documentType: 'sanity.asset',
|
|
45
45
|
projectId: 'test-project-id',
|
|
46
46
|
dataset: 'test-dataset',
|
|
47
|
-
|
|
47
|
+
resourceName: 'media-library',
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
@@ -57,11 +57,11 @@ describe('getResourceIdFromDocumentHandle', () => {
|
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
describe('with DocumentHandleWithSource - canvas', () => {
|
|
60
|
-
it('should return canvas ID and resourceType when canvas
|
|
60
|
+
it('should return canvas ID and resourceType when canvas resource is provided', () => {
|
|
61
61
|
const documentHandle = {
|
|
62
62
|
documentId: 'test-canvas-document-id',
|
|
63
63
|
documentType: 'sanity.canvas.document',
|
|
64
|
-
|
|
64
|
+
resourceName: 'canvas',
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
@@ -73,48 +73,48 @@ describe('getResourceIdFromDocumentHandle', () => {
|
|
|
73
73
|
})
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
describe('with DocumentHandleWithSource - dataset
|
|
77
|
-
it('should return dataset resource ID when dataset
|
|
76
|
+
describe('with DocumentHandleWithSource - dataset resource', () => {
|
|
77
|
+
it('should return dataset resource ID when dataset resource is provided', () => {
|
|
78
78
|
const documentHandle = {
|
|
79
79
|
documentId: 'test-document-id',
|
|
80
80
|
documentType: 'test-document-type',
|
|
81
|
-
|
|
81
|
+
resourceName: 'dataset',
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
85
85
|
|
|
86
86
|
expect(result.current).toEqual({
|
|
87
|
-
id: '
|
|
87
|
+
id: 'resource-project-id.resource-dataset',
|
|
88
88
|
type: undefined,
|
|
89
89
|
})
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
it('should use dataset
|
|
92
|
+
it('should use dataset resource over projectId/dataset when both are provided', () => {
|
|
93
93
|
const documentHandle = {
|
|
94
94
|
documentId: 'test-document-id',
|
|
95
95
|
documentType: 'test-document-type',
|
|
96
96
|
projectId: 'test-project-id',
|
|
97
97
|
dataset: 'test-dataset',
|
|
98
|
-
|
|
98
|
+
resourceName: 'dataset',
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
102
102
|
|
|
103
103
|
expect(result.current).toEqual({
|
|
104
|
-
id: '
|
|
104
|
+
id: 'resource-project-id.resource-dataset',
|
|
105
105
|
type: undefined,
|
|
106
106
|
})
|
|
107
107
|
})
|
|
108
108
|
})
|
|
109
109
|
|
|
110
110
|
describe('edge cases', () => {
|
|
111
|
-
it('should handle DocumentHandleWithSource with undefined
|
|
111
|
+
it('should handle DocumentHandleWithSource with undefined resource', () => {
|
|
112
112
|
const documentHandle = {
|
|
113
113
|
documentId: 'test-document-id',
|
|
114
114
|
documentType: 'test-document-type',
|
|
115
115
|
projectId: 'test-project-id',
|
|
116
116
|
dataset: 'test-dataset',
|
|
117
|
-
|
|
117
|
+
resourceName: undefined,
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type DocumentHandle,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
isCanvasResource,
|
|
4
|
+
isDatasetResource,
|
|
5
|
+
isMediaLibraryResource,
|
|
6
6
|
} from '@sanity/sdk'
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {useNormalizedResourceOptions} from '../../helpers/useNormalizedResourceOptions'
|
|
9
9
|
|
|
10
10
|
interface DashboardMessageResource {
|
|
11
11
|
id: string
|
|
@@ -18,23 +18,23 @@ interface DashboardMessageResource {
|
|
|
18
18
|
export function useResourceIdFromDocumentHandle(
|
|
19
19
|
documentHandle: DocumentHandle,
|
|
20
20
|
): DashboardMessageResource {
|
|
21
|
-
const options =
|
|
22
|
-
const {projectId, dataset,
|
|
21
|
+
const options = useNormalizedResourceOptions(documentHandle)
|
|
22
|
+
const {projectId, dataset, resource} = options
|
|
23
23
|
let resourceId: string = ''
|
|
24
24
|
let resourceType: 'media-library' | 'canvas' | undefined
|
|
25
25
|
if (projectId && dataset) {
|
|
26
26
|
resourceId = `${projectId}.${dataset}`
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
31
|
-
resourceId = `${
|
|
29
|
+
if (resource) {
|
|
30
|
+
if (isDatasetResource(resource)) {
|
|
31
|
+
resourceId = `${resource.projectId}.${resource.dataset}`
|
|
32
32
|
resourceType = undefined
|
|
33
|
-
} else if (
|
|
34
|
-
resourceId =
|
|
33
|
+
} else if (isMediaLibraryResource(resource)) {
|
|
34
|
+
resourceId = resource.mediaLibraryId
|
|
35
35
|
resourceType = 'media-library'
|
|
36
|
-
} else if (
|
|
37
|
-
resourceId =
|
|
36
|
+
} else if (isCanvasResource(resource)) {
|
|
37
|
+
resourceId = resource.canvasId
|
|
38
38
|
resourceType = 'canvas'
|
|
39
39
|
}
|
|
40
40
|
}
|