@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.5
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/README.md +78 -28
- package/dist/_chunks-es/context.js +8 -0
- package/dist/_chunks-es/context.js.map +1 -0
- package/dist/_chunks-es/useLogOut.js +11 -11
- package/dist/_chunks-es/useLogOut.js.map +1 -1
- package/dist/components.d.ts +11 -186
- package/dist/components.js +52 -198
- package/dist/components.js.map +1 -1
- package/dist/context.d.ts +39 -0
- package/dist/context.js +5 -0
- package/dist/context.js.map +1 -0
- package/dist/hooks.d.ts +201 -15
- package/dist/hooks.js +80 -11
- package/dist/hooks.js.map +1 -1
- package/package.json +17 -15
- package/src/_exports/components.ts +2 -13
- package/src/_exports/context.ts +2 -0
- package/src/_exports/hooks.ts +18 -2
- package/src/components/SanityApp.test.tsx +54 -0
- package/src/components/SanityApp.tsx +26 -0
- package/src/components/auth/AuthBoundary.test.tsx +5 -18
- package/src/components/auth/AuthBoundary.tsx +2 -2
- package/src/components/auth/Login.test.tsx +3 -17
- package/src/components/auth/Login.tsx +25 -16
- package/src/components/auth/LoginCallback.test.tsx +2 -17
- package/src/components/auth/LoginCallback.tsx +6 -4
- package/src/components/auth/LoginError.test.tsx +2 -17
- package/src/components/auth/LoginError.tsx +8 -12
- package/src/components/auth/LoginFooter.test.tsx +2 -16
- package/src/components/auth/LoginFooter.tsx +11 -18
- package/src/components/auth/LoginLayout.test.tsx +2 -16
- package/src/components/auth/LoginLayout.tsx +8 -19
- package/src/components/auth/authTestHelpers.tsx +18 -0
- package/src/{components/context → context}/SanityProvider.test.tsx +1 -1
- package/src/hooks/client/useClient.test.tsx +1 -1
- package/src/hooks/comlink/useFrameConnection.test.tsx +122 -0
- package/src/hooks/comlink/useFrameConnection.ts +111 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +94 -0
- package/src/hooks/comlink/useWindowConnection.ts +82 -0
- package/src/hooks/context/useSanityInstance.test.tsx +1 -1
- package/src/hooks/context/useSanityInstance.ts +2 -2
- package/src/hooks/documentCollection/useDocuments.ts +53 -6
- package/src/hooks/helpers/createCallbackHook.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +1 -1
- package/src/hooks/preview/usePreview.test.tsx +17 -8
- package/src/hooks/preview/usePreview.tsx +52 -7
- package/src/vite-env.d.ts +10 -0
- package/dist/assets/bundle-CcAyERuZ.css +0 -11
- package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
- package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
- package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
- package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
- package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
- package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
- package/src/components/Login/LoginLinks.test.tsx +0 -100
- package/src/components/Login/LoginLinks.tsx +0 -73
- package/src/css/css.config.js +0 -220
- package/src/css/paramour.css +0 -2347
- package/src/css/styles.css +0 -11
- /package/src/{components/context → context}/SanityProvider.tsx +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FrameMessage,
|
|
3
|
+
getOrCreateChannel,
|
|
4
|
+
getOrCreateController,
|
|
5
|
+
releaseChannel,
|
|
6
|
+
type WindowMessage,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {useCallback, useEffect, useMemo} from 'react'
|
|
9
|
+
|
|
10
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export type FrameMessageHandler<TWindowMessage extends WindowMessage> = (
|
|
16
|
+
event: TWindowMessage['data'],
|
|
17
|
+
) => TWindowMessage['response'] | Promise<TWindowMessage['response']>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export interface UseFrameConnectionOptions<TWindowMessage extends WindowMessage> {
|
|
23
|
+
name: string
|
|
24
|
+
connectTo: string
|
|
25
|
+
targetOrigin: string
|
|
26
|
+
onMessage?: Record<string, FrameMessageHandler<TWindowMessage>>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export interface FrameConnection<TFrameMessage extends FrameMessage> {
|
|
33
|
+
connect: (frameWindow: Window) => () => void // Return cleanup function
|
|
34
|
+
sendMessage: <T extends TFrameMessage['type']>(
|
|
35
|
+
...params: Extract<TFrameMessage, {type: T}>['data'] extends undefined
|
|
36
|
+
? [type: T]
|
|
37
|
+
: [type: T, data: Extract<TFrameMessage, {type: T}>['data']]
|
|
38
|
+
) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
export function useFrameConnection<
|
|
45
|
+
TFrameMessage extends FrameMessage,
|
|
46
|
+
TWindowMessage extends WindowMessage,
|
|
47
|
+
>(options: UseFrameConnectionOptions<TWindowMessage>): FrameConnection<TFrameMessage> {
|
|
48
|
+
const {onMessage, targetOrigin, name, connectTo} = options
|
|
49
|
+
const instance = useSanityInstance()
|
|
50
|
+
|
|
51
|
+
const controller = useMemo(
|
|
52
|
+
() => getOrCreateController(instance, targetOrigin),
|
|
53
|
+
[instance, targetOrigin],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const channel = useMemo(
|
|
57
|
+
() =>
|
|
58
|
+
getOrCreateChannel(instance, {
|
|
59
|
+
name,
|
|
60
|
+
connectTo,
|
|
61
|
+
}),
|
|
62
|
+
[instance, name, connectTo],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!channel || !onMessage) return
|
|
67
|
+
|
|
68
|
+
const unsubscribers: Array<() => void> = []
|
|
69
|
+
|
|
70
|
+
Object.entries(onMessage).forEach(([type, handler]) => {
|
|
71
|
+
const unsubscribe = channel.on(type, handler)
|
|
72
|
+
unsubscribers.push(unsubscribe)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
unsubscribers.forEach((unsub) => unsub())
|
|
77
|
+
}
|
|
78
|
+
}, [channel, onMessage])
|
|
79
|
+
|
|
80
|
+
const connect = useCallback(
|
|
81
|
+
(frameWindow: Window) => {
|
|
82
|
+
const removeTarget = controller?.addTarget(frameWindow)
|
|
83
|
+
return () => {
|
|
84
|
+
removeTarget?.()
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[controller],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const sendMessage = useCallback(
|
|
91
|
+
<T extends TFrameMessage['type']>(
|
|
92
|
+
type: T,
|
|
93
|
+
data?: Extract<TFrameMessage, {type: T}>['data'],
|
|
94
|
+
) => {
|
|
95
|
+
channel?.post(type, data)
|
|
96
|
+
},
|
|
97
|
+
[channel],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// cleanup channel on unmount
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
return () => {
|
|
103
|
+
releaseChannel(instance, name)
|
|
104
|
+
}
|
|
105
|
+
}, [name, instance])
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
connect,
|
|
109
|
+
sendMessage,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {type Message, type Node} from '@sanity/comlink'
|
|
2
|
+
import {getOrCreateNode, releaseNode} from '@sanity/sdk'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {renderHook} from '../../../test/test-utils'
|
|
6
|
+
import {useWindowConnection} from './useWindowConnection'
|
|
7
|
+
|
|
8
|
+
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal()
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
getOrCreateNode: vi.fn(),
|
|
13
|
+
createNode: vi.fn(),
|
|
14
|
+
releaseNode: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
interface TestMessage {
|
|
18
|
+
type: 'TEST_MESSAGE'
|
|
19
|
+
data: {someData: string}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AnotherMessage {
|
|
23
|
+
type: 'ANOTHER_MESSAGE'
|
|
24
|
+
data: {otherData: number}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type TestMessages = TestMessage | AnotherMessage
|
|
28
|
+
|
|
29
|
+
function createMockNode() {
|
|
30
|
+
return {
|
|
31
|
+
// return unsubscribe function
|
|
32
|
+
on: vi.fn(() => () => {}),
|
|
33
|
+
post: vi.fn(),
|
|
34
|
+
stop: vi.fn(),
|
|
35
|
+
} as unknown as Node<Message, Message>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('useWindowConnection', () => {
|
|
39
|
+
let node: Node<Message, Message>
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
node = createMockNode()
|
|
43
|
+
|
|
44
|
+
vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should register message handlers', () => {
|
|
48
|
+
const mockHandler = vi.fn()
|
|
49
|
+
const mockData = {someData: 'test'}
|
|
50
|
+
|
|
51
|
+
renderHook(() =>
|
|
52
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
53
|
+
name: 'test',
|
|
54
|
+
connectTo: 'window',
|
|
55
|
+
onMessage: {
|
|
56
|
+
TEST_MESSAGE: mockHandler,
|
|
57
|
+
ANOTHER_MESSAGE: vi.fn(),
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const onCallback = vi.mocked(node.on).mock.calls[0][1]
|
|
63
|
+
onCallback(mockData)
|
|
64
|
+
|
|
65
|
+
expect(mockHandler).toHaveBeenCalledWith(mockData)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should send messages through the node', () => {
|
|
69
|
+
const {result} = renderHook(() =>
|
|
70
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
71
|
+
name: 'test',
|
|
72
|
+
connectTo: 'window',
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
result.current.sendMessage('TEST_MESSAGE', {someData: 'test'})
|
|
77
|
+
expect(node.post).toHaveBeenCalledWith('TEST_MESSAGE', {someData: 'test'})
|
|
78
|
+
|
|
79
|
+
result.current.sendMessage('ANOTHER_MESSAGE', {otherData: 123})
|
|
80
|
+
expect(node.post).toHaveBeenCalledWith('ANOTHER_MESSAGE', {otherData: 123})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should cleanup on unmount', () => {
|
|
84
|
+
const {unmount} = renderHook(() =>
|
|
85
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
86
|
+
name: 'test',
|
|
87
|
+
connectTo: 'window',
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
unmount()
|
|
92
|
+
expect(releaseNode).toHaveBeenCalled()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
|
|
2
|
+
import {useCallback, useEffect, useMemo} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export type WindowMessageHandler<TFrameMessage extends FrameMessage> = (
|
|
10
|
+
event: TFrameMessage['data'],
|
|
11
|
+
) => TFrameMessage['response']
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export interface UseWindowConnectionOptions<TMessage extends FrameMessage> {
|
|
17
|
+
name: string
|
|
18
|
+
connectTo: string
|
|
19
|
+
onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
export interface WindowConnection<TMessage extends WindowMessage> {
|
|
26
|
+
sendMessage: <TType extends TMessage['type']>(
|
|
27
|
+
type: TType,
|
|
28
|
+
data?: Extract<TMessage, {type: TType}>['data'],
|
|
29
|
+
) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @public
|
|
34
|
+
*/
|
|
35
|
+
export function useWindowConnection<
|
|
36
|
+
TWindowMessage extends WindowMessage,
|
|
37
|
+
TFrameMessage extends FrameMessage,
|
|
38
|
+
>(options: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {
|
|
39
|
+
const {name, onMessage, connectTo} = options
|
|
40
|
+
const instance = useSanityInstance()
|
|
41
|
+
|
|
42
|
+
const node = useMemo(
|
|
43
|
+
() => getOrCreateNode(instance, {name, connectTo}),
|
|
44
|
+
[instance, name, connectTo],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!onMessage) return
|
|
49
|
+
|
|
50
|
+
const unsubscribers: Array<() => void> = []
|
|
51
|
+
|
|
52
|
+
Object.entries(onMessage).forEach(([type, handler]) => {
|
|
53
|
+
const unsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)
|
|
54
|
+
unsubscribers.push(unsubscribe)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
unsubscribers.forEach((unsub) => unsub())
|
|
59
|
+
}
|
|
60
|
+
}, [node, onMessage])
|
|
61
|
+
|
|
62
|
+
const sendMessage = useCallback(
|
|
63
|
+
<TType extends WindowMessage['type']>(
|
|
64
|
+
type: TType,
|
|
65
|
+
data?: Extract<WindowMessage, {type: TType}>['data'],
|
|
66
|
+
) => {
|
|
67
|
+
node?.post(type, data)
|
|
68
|
+
},
|
|
69
|
+
[node],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
// cleanup node on unmount
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
return () => {
|
|
75
|
+
releaseNode(instance, name)
|
|
76
|
+
}
|
|
77
|
+
}, [instance, name])
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
sendMessage,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -3,7 +3,7 @@ import {renderHook} from '@testing-library/react'
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import {describe, expect, it, vi} from 'vitest'
|
|
5
5
|
|
|
6
|
-
import {SanityProvider} from '../../
|
|
6
|
+
import {SanityProvider} from '../../context/SanityProvider'
|
|
7
7
|
import {useSanityInstance} from './useSanityInstance'
|
|
8
8
|
|
|
9
9
|
describe('useSanityInstance', () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {useContext} from 'react'
|
|
3
3
|
|
|
4
|
-
import {SanityInstanceContext} from '../../
|
|
4
|
+
import {SanityInstanceContext} from '../../context/SanityProvider'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Hook that provides the current Sanity instance from the context.
|
|
@@ -6,11 +6,16 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
6
6
|
/**
|
|
7
7
|
* @public
|
|
8
8
|
*/
|
|
9
|
-
export interface
|
|
9
|
+
export interface DocumentCollection {
|
|
10
|
+
/** Retrieve more documents matching the provided options */
|
|
10
11
|
loadMore: () => void
|
|
12
|
+
/** The retrieved document handles of the documents matching the provided options */
|
|
11
13
|
results: DocumentHandle[]
|
|
14
|
+
/** Whether a retrieval of documents is in flight */
|
|
12
15
|
isPending: boolean
|
|
16
|
+
/** Whether more documents exist that match the provided options than have been retrieved */
|
|
13
17
|
hasMore: boolean
|
|
18
|
+
/** The total number of documents in the collection */
|
|
14
19
|
count: number
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -24,14 +29,56 @@ const STABLE_EMPTY = {
|
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
|
-
* Hook to get the list of documents for specified options
|
|
28
|
-
*
|
|
29
32
|
* @public
|
|
30
33
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* The `useDocuments` hook retrieves and provides access to a live collection of documents, optionally filtered, sorted, and matched to a given Content Lake perspective.
|
|
35
|
+
* Because the returned document collection is live, the results will update in real time until the component invoking the hook is unmounted.
|
|
36
|
+
*
|
|
37
|
+
* @param options - Options for narrowing and sorting the document collection
|
|
38
|
+
* @returns The collection of documents matching the provided options (if any), as well as properties describing the collection and a function to load more.
|
|
39
|
+
*
|
|
40
|
+
* @example Retrieving all documents of type 'movie'
|
|
41
|
+
* ```
|
|
42
|
+
* const { results, isPending } = useDocuments({ filter: '_type == "movie"' })
|
|
43
|
+
*
|
|
44
|
+
* return (
|
|
45
|
+
* <div>
|
|
46
|
+
* <h1>Movies</h1>
|
|
47
|
+
* {results && (
|
|
48
|
+
* <ul>
|
|
49
|
+
* {results.map(movie => (<li key={movie._id}>…</li>))}
|
|
50
|
+
* </ul>
|
|
51
|
+
* )}
|
|
52
|
+
* {isPending && <div>Loading movies…</div>}
|
|
53
|
+
* </div>
|
|
54
|
+
* )
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example Retrieving all movies released since 1980, sorted by release date
|
|
58
|
+
* ```
|
|
59
|
+
* const { results } = useDocuments({
|
|
60
|
+
* filter: '_type == "movie" && releaseDate >= "1980-01-01"',
|
|
61
|
+
* sort: [
|
|
62
|
+
* {
|
|
63
|
+
* field: 'releaseDate',
|
|
64
|
+
* sort: 'asc',
|
|
65
|
+
* },
|
|
66
|
+
* ],
|
|
67
|
+
* })
|
|
68
|
+
*
|
|
69
|
+
* return (
|
|
70
|
+
* <div>
|
|
71
|
+
* <h1>Movies released since 1980</h1>
|
|
72
|
+
* {results && (
|
|
73
|
+
* <ol>
|
|
74
|
+
* {results.map(movie => (<li key={movie._id}>…</li>))}
|
|
75
|
+
* </ol>
|
|
76
|
+
* )}
|
|
77
|
+
* </div>
|
|
78
|
+
* )
|
|
79
|
+
* ```
|
|
33
80
|
*/
|
|
34
|
-
export function useDocuments(options: DocumentListOptions = {}):
|
|
81
|
+
export function useDocuments(options: DocumentListOptions = {}): DocumentCollection {
|
|
35
82
|
const instance = useSanityInstance()
|
|
36
83
|
|
|
37
84
|
// NOTE: useState is used because it guaranteed to return a stable reference
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {type DocumentHandle, getPreviewState, type PreviewValue, resolvePreview} from '@sanity/sdk'
|
|
2
2
|
import {act, render, screen} from '@testing-library/react'
|
|
3
3
|
import {Suspense, useState} from 'react'
|
|
4
|
-
import type
|
|
4
|
+
import {type Mock} from 'vitest'
|
|
5
5
|
|
|
6
6
|
import {usePreview} from './usePreview'
|
|
7
7
|
|
|
@@ -45,13 +45,13 @@ const mockDocument: DocumentHandle = {
|
|
|
45
45
|
|
|
46
46
|
function TestComponent({document}: {document: DocumentHandle}) {
|
|
47
47
|
const [ref, setRef] = useState<HTMLElement | null>(null)
|
|
48
|
-
const
|
|
48
|
+
const {results, isPending} = usePreview({document, ref})
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
51
|
<div ref={setRef}>
|
|
52
|
-
<h1>{
|
|
53
|
-
<p>{
|
|
54
|
-
{
|
|
52
|
+
<h1>{results?.title}</h1>
|
|
53
|
+
<p>{results?.subtitle}</p>
|
|
54
|
+
{isPending && <div>Pending...</div>}
|
|
55
55
|
</div>
|
|
56
56
|
)
|
|
57
57
|
}
|
|
@@ -74,7 +74,10 @@ describe('usePreview', () => {
|
|
|
74
74
|
|
|
75
75
|
test('it only subscribes when element is visible', async () => {
|
|
76
76
|
// Setup initial state
|
|
77
|
-
getCurrent.mockReturnValue(
|
|
77
|
+
getCurrent.mockReturnValue({
|
|
78
|
+
results: {title: 'Initial Title', subtitle: 'Initial Subtitle'},
|
|
79
|
+
isPending: false,
|
|
80
|
+
})
|
|
78
81
|
const eventsUnsubscribe = vi.fn()
|
|
79
82
|
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
80
83
|
|
|
@@ -135,7 +138,10 @@ describe('usePreview', () => {
|
|
|
135
138
|
await act(async () => {
|
|
136
139
|
intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
|
|
137
140
|
await resolvePromise
|
|
138
|
-
getCurrent.mockReturnValue(
|
|
141
|
+
getCurrent.mockReturnValue({
|
|
142
|
+
results: {title: 'Resolved Title', subtitle: 'Resolved Subtitle'},
|
|
143
|
+
isPending: false,
|
|
144
|
+
})
|
|
139
145
|
subscriber?.()
|
|
140
146
|
})
|
|
141
147
|
|
|
@@ -149,7 +155,10 @@ describe('usePreview', () => {
|
|
|
149
155
|
// @ts-expect-error - Intentionally removing IntersectionObserver
|
|
150
156
|
delete window.IntersectionObserver
|
|
151
157
|
|
|
152
|
-
getCurrent.mockReturnValue(
|
|
158
|
+
getCurrent.mockReturnValue({
|
|
159
|
+
results: {title: 'Fallback Title', subtitle: 'Fallback Subtitle'},
|
|
160
|
+
isPending: false,
|
|
161
|
+
})
|
|
153
162
|
subscribe.mockImplementation(() => vi.fn())
|
|
154
163
|
|
|
155
164
|
render(
|
|
@@ -15,10 +15,55 @@ export interface UsePreviewOptions {
|
|
|
15
15
|
/**
|
|
16
16
|
* @alpha
|
|
17
17
|
*/
|
|
18
|
-
export
|
|
19
|
-
document
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
export interface UsePreviewResults {
|
|
19
|
+
/** The results of resolving the document’s preview values */
|
|
20
|
+
results: PreviewValue
|
|
21
|
+
/** Whether the resolution of the preview values is pending */
|
|
22
|
+
isPending: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @alpha
|
|
27
|
+
*
|
|
28
|
+
* The `usePreview` hook takes a document (via a `DocumentHandle`) and returns its resolved preview values,
|
|
29
|
+
* including the document’s `title`, `subtitle`, `media`, and `status`. These values are live and will update in realtime.
|
|
30
|
+
* To reduce unnecessary network requests for resolving the preview values, an optional `ref` can be passed to the hook so that preview
|
|
31
|
+
* resolution will only occur if the `ref` is intersecting the current viewport.
|
|
32
|
+
*
|
|
33
|
+
* @param options - The document handle for the document you want to resolve preview values for, and an optional ref
|
|
34
|
+
* @returns The preview values for the given document and a boolean to indicate whether the resolution is pending
|
|
35
|
+
*
|
|
36
|
+
* @example Combining with useDocuments to render a collection of document previews
|
|
37
|
+
* ```
|
|
38
|
+
* // PreviewComponent.jsx
|
|
39
|
+
* export default function PreviewComponent({ document }) {
|
|
40
|
+
* const { results: { title, subtitle, media }, isPending } = usePreview({ document })
|
|
41
|
+
* return isPending ? 'Loading…' : (
|
|
42
|
+
* <article>
|
|
43
|
+
* {media?.type === 'image-asset' ? <img src={media.url} alt='' /> : ''}
|
|
44
|
+
* <h2>{title}</h2>
|
|
45
|
+
* <p>{subtitle}</p>
|
|
46
|
+
* </article>
|
|
47
|
+
* )
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* // DocumentList.jsx
|
|
51
|
+
* const { results, isPending } = useDocuments({ filter: '_type == "movie"' })
|
|
52
|
+
* return (
|
|
53
|
+
* <div>
|
|
54
|
+
* <h1>Movies</h1>
|
|
55
|
+
* <ul>
|
|
56
|
+
* {isPending ? 'Loading…' : results.map(movie => (
|
|
57
|
+
* <li key={movie._id}>
|
|
58
|
+
* <PreviewComponent document={movie} />
|
|
59
|
+
* </li>
|
|
60
|
+
* ))}
|
|
61
|
+
* </ul>
|
|
62
|
+
* </div>
|
|
63
|
+
* )
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function usePreview({document: {_id, _type}, ref}: UsePreviewOptions): UsePreviewResults {
|
|
22
67
|
const instance = useSanityInstance()
|
|
23
68
|
|
|
24
69
|
const stateSource = useMemo(
|
|
@@ -60,9 +105,9 @@ export function usePreview({
|
|
|
60
105
|
|
|
61
106
|
// Create getSnapshot function to return current state
|
|
62
107
|
const getSnapshot = useCallback(() => {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
return
|
|
108
|
+
const currentState = stateSource.getCurrent()
|
|
109
|
+
if (currentState.results === null) throw resolvePreview(instance, {document: {_id, _type}})
|
|
110
|
+
return currentState as UsePreviewResults
|
|
66
111
|
}, [_id, _type, instance, stateSource])
|
|
67
112
|
|
|
68
113
|
return useSyncExternalStore(subscribe, getSnapshot)
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import {type Meta, type StoryObj} from '@storybook/react'
|
|
2
|
-
|
|
3
|
-
import {DocumentPreviewLayout} from '../DocumentPreviewLayout/DocumentPreviewLayout.tsx'
|
|
4
|
-
import {DocumentGridLayout} from './DocumentGridLayout.tsx'
|
|
5
|
-
|
|
6
|
-
const meta: Meta<typeof DocumentGridLayout> = {
|
|
7
|
-
title: 'DocumentGridLayout',
|
|
8
|
-
component: DocumentGridLayout,
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export default meta
|
|
12
|
-
type Story = StoryObj<typeof meta>
|
|
13
|
-
|
|
14
|
-
const mockDocs = [
|
|
15
|
-
{id: '1', title: 'Just a title', url: '#', docType: 'article', status: 'published'},
|
|
16
|
-
{
|
|
17
|
-
id: '2',
|
|
18
|
-
title: 'A title, but also',
|
|
19
|
-
subtitle: 'A subtitle',
|
|
20
|
-
docType: 'article',
|
|
21
|
-
status: 'draft',
|
|
22
|
-
media: {
|
|
23
|
-
type: 'image-asset',
|
|
24
|
-
url: 'https://picsum.photos/75/75',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: '3',
|
|
29
|
-
title: 'Hello World',
|
|
30
|
-
subtitle: 'What a nice list I get to live in',
|
|
31
|
-
docType: 'image',
|
|
32
|
-
status: 'published',
|
|
33
|
-
media: {
|
|
34
|
-
type: 'image-asset',
|
|
35
|
-
url: 'https://picsum.photos/80/80',
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: '4',
|
|
40
|
-
title: 'I’ve been selected',
|
|
41
|
-
subtitle: 'I feel special',
|
|
42
|
-
selected: true,
|
|
43
|
-
docType: 'video',
|
|
44
|
-
status: 'draft',
|
|
45
|
-
media: {
|
|
46
|
-
type: 'image-asset',
|
|
47
|
-
url: 'https://picsum.photos/100/100',
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: '5',
|
|
52
|
-
title: 'A very long title that at some point might get truncated if it goes for long enough',
|
|
53
|
-
subtitle:
|
|
54
|
-
'Along with a subtitle that is quite long as well, in order to demonstrate the truncation of its text',
|
|
55
|
-
docType: 'audio',
|
|
56
|
-
status: 'published',
|
|
57
|
-
media: {
|
|
58
|
-
type: 'image-asset',
|
|
59
|
-
url: 'https://picsum.photos/75/75',
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: '6',
|
|
64
|
-
title: 'Hello World',
|
|
65
|
-
subtitle: 'What a nice list I get to live in',
|
|
66
|
-
docType: 'pdf',
|
|
67
|
-
status: 'published',
|
|
68
|
-
media: {
|
|
69
|
-
type: 'image-asset',
|
|
70
|
-
url: 'https://picsum.photos/75/75',
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
{id: '7', title: 'Just a title', url: '#', docType: 'note', status: 'published,'},
|
|
74
|
-
{
|
|
75
|
-
id: '8',
|
|
76
|
-
title: 'A title, but also',
|
|
77
|
-
subtitle: 'A subtitle',
|
|
78
|
-
docType: 'document',
|
|
79
|
-
status: 'draft',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
id: '9',
|
|
83
|
-
title: 'Hello World',
|
|
84
|
-
subtitle: 'What a nice list I get to live in',
|
|
85
|
-
docType: 'biography',
|
|
86
|
-
status: 'published',
|
|
87
|
-
media: {
|
|
88
|
-
type: 'image-asset',
|
|
89
|
-
url: 'https://picsum.photos/200/200',
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
]
|
|
93
|
-
|
|
94
|
-
export const Default: Story = {
|
|
95
|
-
render: () => {
|
|
96
|
-
return (
|
|
97
|
-
<DocumentGridLayout>
|
|
98
|
-
{mockDocs.map((doc) => (
|
|
99
|
-
<li key={doc.id}>
|
|
100
|
-
<DocumentPreviewLayout
|
|
101
|
-
title={doc.title}
|
|
102
|
-
subtitle={doc.subtitle}
|
|
103
|
-
media={doc.media}
|
|
104
|
-
selected={doc.selected}
|
|
105
|
-
docType={doc.docType}
|
|
106
|
-
status={doc.status}
|
|
107
|
-
/>
|
|
108
|
-
</li>
|
|
109
|
-
))}
|
|
110
|
-
</DocumentGridLayout>
|
|
111
|
-
)
|
|
112
|
-
},
|
|
113
|
-
}
|