@sanity/sdk-react 0.0.0-alpha.4 → 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 +79 -26
- package/dist/components.d.ts +1 -0
- package/dist/components.js +47 -24
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +111 -16
- package/dist/hooks.js +4 -7
- package/dist/hooks.js.map +1 -1
- package/package.json +5 -4
- package/src/_exports/hooks.ts +6 -2
- package/src/components/auth/AuthBoundary.test.tsx +4 -10
- package/src/components/auth/Login.test.tsx +3 -10
- package/src/components/auth/Login.tsx +27 -11
- package/src/components/auth/LoginCallback.test.tsx +2 -9
- package/src/components/auth/LoginCallback.tsx +7 -4
- package/src/components/auth/LoginError.test.tsx +2 -10
- package/src/components/auth/LoginError.tsx +8 -12
- package/src/components/auth/LoginFooter.test.tsx +2 -9
- package/src/components/auth/LoginFooter.tsx +20 -13
- package/src/components/auth/LoginLayout.test.tsx +2 -9
- package/src/components/auth/LoginLayout.tsx +9 -8
- package/src/components/auth/authTestHelpers.tsx +18 -0
- package/src/hooks/documentCollection/useDocuments.ts +53 -6
- package/src/hooks/preview/usePreview.test.tsx +16 -7
- package/src/hooks/preview/usePreview.tsx +52 -7
- package/src/components/Login/LoginLinks.test.tsx +0 -91
- package/src/components/Login/LoginLinks.tsx +0 -58
package/src/_exports/hooks.ts
CHANGED
|
@@ -17,5 +17,9 @@ export {
|
|
|
17
17
|
type WindowMessageHandler,
|
|
18
18
|
} from '../hooks/comlink/useWindowConnection'
|
|
19
19
|
export {useSanityInstance} from '../hooks/context/useSanityInstance'
|
|
20
|
-
export {type
|
|
21
|
-
export {
|
|
20
|
+
export {type DocumentCollection, useDocuments} from '../hooks/documentCollection/useDocuments'
|
|
21
|
+
export {
|
|
22
|
+
usePreview,
|
|
23
|
+
type UsePreviewOptions,
|
|
24
|
+
type UsePreviewResults,
|
|
25
|
+
} from '../hooks/preview/usePreview'
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {AuthStateType
|
|
2
|
-
import {SanityProvider} from '@sanity/sdk-react/context'
|
|
1
|
+
import {AuthStateType} from '@sanity/sdk'
|
|
3
2
|
import {useAuthState} from '@sanity/sdk-react/hooks'
|
|
4
|
-
import {
|
|
5
|
-
import React from 'react'
|
|
3
|
+
import {screen, waitFor} from '@testing-library/react'
|
|
6
4
|
import {beforeEach, describe, expect, it, type MockInstance, vi} from 'vitest'
|
|
7
5
|
|
|
8
6
|
import {AuthBoundary} from './AuthBoundary'
|
|
7
|
+
import {renderWithWrappers} from './authTestHelpers'
|
|
9
8
|
|
|
10
9
|
// Mock hooks
|
|
11
10
|
vi.mock('../../hooks/auth/useAuthState', () => ({
|
|
@@ -36,11 +35,6 @@ vi.mock('./AuthError', async (importOriginal) => {
|
|
|
36
35
|
}
|
|
37
36
|
})
|
|
38
37
|
|
|
39
|
-
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
40
|
-
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
41
|
-
return render(<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
38
|
describe('AuthBoundary', () => {
|
|
45
39
|
let consoleErrorSpy: MockInstance
|
|
46
40
|
beforeEach(() => {
|
|
@@ -60,7 +54,7 @@ describe('AuthBoundary', () => {
|
|
|
60
54
|
renderWithWrappers(<AuthBoundary>Protected Content</AuthBoundary>)
|
|
61
55
|
|
|
62
56
|
// The login screen should show "Choose login provider" by default
|
|
63
|
-
expect(screen.getByText('Choose login provider')).toBeInTheDocument()
|
|
57
|
+
expect(screen.getByText('Choose login provider:')).toBeInTheDocument()
|
|
64
58
|
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument()
|
|
65
59
|
})
|
|
66
60
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {SanityProvider} from '@sanity/sdk-react/context'
|
|
3
|
-
import {render, screen} from '@testing-library/react'
|
|
4
|
-
import React from 'react'
|
|
1
|
+
import {screen} from '@testing-library/react'
|
|
5
2
|
import {describe, expect, it, vi} from 'vitest'
|
|
6
3
|
|
|
4
|
+
import {renderWithWrappers} from './authTestHelpers'
|
|
7
5
|
import {Login} from './Login'
|
|
8
6
|
|
|
9
7
|
vi.mock('../../hooks/auth/useLoginUrls', () => ({
|
|
@@ -13,15 +11,10 @@ vi.mock('../../hooks/auth/useLoginUrls', () => ({
|
|
|
13
11
|
]),
|
|
14
12
|
}))
|
|
15
13
|
|
|
16
|
-
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
17
|
-
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
18
|
-
return render(<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
14
|
describe('Login', () => {
|
|
22
15
|
it('renders login providers', () => {
|
|
23
16
|
renderWithWrappers(<Login />)
|
|
24
|
-
expect(screen.getByText('Choose login provider')).toBeInTheDocument()
|
|
17
|
+
expect(screen.getByText('Choose login provider:')).toBeInTheDocument()
|
|
25
18
|
expect(screen.getByRole('link', {name: 'Provider A'})).toHaveAttribute(
|
|
26
19
|
'href',
|
|
27
20
|
'https://provider-a.com/auth',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {Box, Button, Flex, Heading, Spinner, Stack} from '@sanity/ui'
|
|
1
2
|
import {type JSX, Suspense} from 'react'
|
|
2
3
|
|
|
3
4
|
import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
|
|
@@ -8,17 +9,26 @@ import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
|
8
9
|
* Renders a list of login options with a loading fallback while providers load.
|
|
9
10
|
*
|
|
10
11
|
* @alpha
|
|
12
|
+
* @internal
|
|
11
13
|
*/
|
|
12
14
|
export function Login({header, footer}: LoginLayoutProps): JSX.Element {
|
|
13
15
|
return (
|
|
14
16
|
<LoginLayout header={header} footer={footer}>
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
+
<Heading as="h6" align="center">
|
|
18
|
+
Choose login provider:
|
|
19
|
+
</Heading>
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
<Suspense
|
|
22
|
+
fallback={
|
|
23
|
+
<Box padding={5}>
|
|
24
|
+
<Flex align="center" justify="center">
|
|
25
|
+
<Spinner />
|
|
26
|
+
</Flex>
|
|
27
|
+
</Box>
|
|
28
|
+
}
|
|
29
|
+
>
|
|
30
|
+
<Providers />
|
|
31
|
+
</Suspense>
|
|
22
32
|
</LoginLayout>
|
|
23
33
|
)
|
|
24
34
|
}
|
|
@@ -27,12 +37,18 @@ function Providers() {
|
|
|
27
37
|
const loginUrls = useLoginUrls()
|
|
28
38
|
|
|
29
39
|
return (
|
|
30
|
-
<
|
|
40
|
+
<Stack space={3} marginY={5}>
|
|
31
41
|
{loginUrls.map(({title, url}) => (
|
|
32
|
-
<
|
|
33
|
-
{
|
|
34
|
-
|
|
42
|
+
<Button
|
|
43
|
+
key={url}
|
|
44
|
+
as="a"
|
|
45
|
+
href={url}
|
|
46
|
+
mode="ghost"
|
|
47
|
+
text={title}
|
|
48
|
+
textAlign="center"
|
|
49
|
+
fontSize={2}
|
|
50
|
+
></Button>
|
|
35
51
|
))}
|
|
36
|
-
</
|
|
52
|
+
</Stack>
|
|
37
53
|
)
|
|
38
54
|
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {SanityProvider} from '@sanity/sdk-react/context'
|
|
3
|
-
import {render, screen, waitFor} from '@testing-library/react'
|
|
4
|
-
import React from 'react'
|
|
1
|
+
import {screen, waitFor} from '@testing-library/react'
|
|
5
2
|
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
10
|
-
return render(<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>)
|
|
11
|
-
}
|
|
4
|
+
import {renderWithWrappers} from './authTestHelpers'
|
|
12
5
|
|
|
13
6
|
// Mock `useHandleCallback`
|
|
14
7
|
vi.mock('../../hooks/auth/useHandleCallback', () => ({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {Flex, Heading, Spinner} from '@sanity/ui'
|
|
1
2
|
import {useEffect} from 'react'
|
|
2
3
|
|
|
3
4
|
import {useHandleCallback} from '../../hooks/auth/useHandleCallback'
|
|
@@ -27,10 +28,12 @@ export function LoginCallback({header, footer}: LoginLayoutProps): React.ReactNo
|
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
30
|
<LoginLayout header={header} footer={footer}>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
<Heading as="h6" align="center">
|
|
32
|
+
Logging you in…
|
|
33
|
+
</Heading>
|
|
34
|
+
<Flex paddingY={5} align="center" justify="center">
|
|
35
|
+
<Spinner />
|
|
36
|
+
</Flex>
|
|
34
37
|
</LoginLayout>
|
|
35
38
|
)
|
|
36
39
|
}
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {SanityProvider} from '@sanity/sdk-react/context'
|
|
3
|
-
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
|
4
|
-
import React from 'react'
|
|
1
|
+
import {fireEvent, screen, waitFor} from '@testing-library/react'
|
|
5
2
|
import {describe, expect, it, vi} from 'vitest'
|
|
6
3
|
|
|
7
4
|
import {AuthError} from './AuthError'
|
|
5
|
+
import {renderWithWrappers} from './authTestHelpers'
|
|
8
6
|
import {LoginError} from './LoginError'
|
|
9
7
|
|
|
10
8
|
vi.mock('../../hooks/auth/useLogOut', () => ({
|
|
11
9
|
useLogOut: vi.fn(() => async () => {}),
|
|
12
10
|
}))
|
|
13
11
|
|
|
14
|
-
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
15
|
-
|
|
16
|
-
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
17
|
-
return render(<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
12
|
describe('LoginError', () => {
|
|
21
13
|
it('shows authentication error and retry button', async () => {
|
|
22
14
|
const mockReset = vi.fn()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {Button, Heading, Stack, Text} from '@sanity/ui'
|
|
1
2
|
import {useCallback} from 'react'
|
|
2
3
|
import {type FallbackProps} from 'react-error-boundary'
|
|
3
4
|
|
|
@@ -32,18 +33,13 @@ export function LoginError({
|
|
|
32
33
|
|
|
33
34
|
return (
|
|
34
35
|
<LoginLayout header={header} footer={footer}>
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<button className="sc-login-error__button" onClick={handleRetry}>
|
|
44
|
-
Retry
|
|
45
|
-
</button>
|
|
46
|
-
</div>
|
|
36
|
+
<Stack space={5} marginBottom={5}>
|
|
37
|
+
<Heading as="h6" align="center">
|
|
38
|
+
Authentication Error
|
|
39
|
+
</Heading>
|
|
40
|
+
<Text align="center">Please try again or contact support if the problem persists.</Text>
|
|
41
|
+
<Button mode="ghost" onClick={handleRetry} text="Retry" fontSize={2} />
|
|
42
|
+
</Stack>
|
|
47
43
|
</LoginLayout>
|
|
48
44
|
)
|
|
49
45
|
}
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {SanityProvider} from '@sanity/sdk-react/context'
|
|
3
|
-
import {render, screen} from '@testing-library/react'
|
|
4
|
-
import React from 'react'
|
|
1
|
+
import {screen} from '@testing-library/react'
|
|
5
2
|
import {describe, expect, it} from 'vitest'
|
|
6
3
|
|
|
4
|
+
import {renderWithWrappers} from './authTestHelpers'
|
|
7
5
|
import {LoginFooter} from './LoginFooter'
|
|
8
6
|
|
|
9
|
-
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
10
|
-
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
11
|
-
return render(<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
7
|
describe('LoginFooter', () => {
|
|
15
8
|
it('renders footer links', () => {
|
|
16
9
|
renderWithWrappers(<LoginFooter />)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {SanityLogo} from '@sanity/logos'
|
|
2
|
-
import {
|
|
2
|
+
import {Box, Flex, Inline, Text} from '@sanity/ui'
|
|
3
3
|
|
|
4
4
|
const LINKS = [
|
|
5
5
|
{
|
|
@@ -32,20 +32,27 @@ const LINKS = [
|
|
|
32
32
|
*/
|
|
33
33
|
export function LoginFooter(): React.ReactNode {
|
|
34
34
|
return (
|
|
35
|
-
<
|
|
36
|
-
<
|
|
35
|
+
<Box>
|
|
36
|
+
<Flex justify="center">
|
|
37
|
+
<SanityLogo />
|
|
38
|
+
</Flex>
|
|
37
39
|
|
|
38
|
-
<
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
<
|
|
42
|
-
<a
|
|
40
|
+
<Flex justify="center">
|
|
41
|
+
<Inline space={2} paddingY={3}>
|
|
42
|
+
{LINKS.map((link) => (
|
|
43
|
+
<Text size={0} key={link.url}>
|
|
44
|
+
<a
|
|
45
|
+
href={link.url}
|
|
46
|
+
target="_blank"
|
|
47
|
+
rel="noopener noreferrer"
|
|
48
|
+
style={{color: 'inherit'}}
|
|
49
|
+
>
|
|
43
50
|
{link.title}
|
|
44
51
|
</a>
|
|
45
|
-
</
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
</
|
|
49
|
-
</
|
|
52
|
+
</Text>
|
|
53
|
+
))}
|
|
54
|
+
</Inline>
|
|
55
|
+
</Flex>
|
|
56
|
+
</Box>
|
|
50
57
|
)
|
|
51
58
|
}
|
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {SanityProvider} from '@sanity/sdk-react/context'
|
|
3
|
-
import {render, screen} from '@testing-library/react'
|
|
4
|
-
import React from 'react'
|
|
1
|
+
import {screen} from '@testing-library/react'
|
|
5
2
|
import {describe, expect, it} from 'vitest'
|
|
6
3
|
|
|
4
|
+
import {renderWithWrappers} from './authTestHelpers'
|
|
7
5
|
import {LoginLayout} from './LoginLayout'
|
|
8
6
|
|
|
9
|
-
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
10
|
-
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
11
|
-
return render(<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
7
|
describe('LoginLayout', () => {
|
|
15
8
|
it('renders header, children, and footer', () => {
|
|
16
9
|
renderWithWrappers(
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {Card, Container} from '@sanity/ui'
|
|
2
|
+
|
|
1
3
|
import {LoginFooter} from './LoginFooter'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* @alpha
|
|
7
|
+
* @internal
|
|
5
8
|
*/
|
|
6
9
|
export interface LoginLayoutProps {
|
|
7
10
|
/** Optional header content rendered at top of card */
|
|
@@ -54,16 +57,14 @@ export function LoginLayout({
|
|
|
54
57
|
header,
|
|
55
58
|
}: LoginLayoutProps): React.ReactNode {
|
|
56
59
|
return (
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
{header && <div className="sc-login-layout__card-header">{header}</div>}
|
|
60
|
+
<Container width={0}>
|
|
61
|
+
<Card shadow={1} radius={2} padding={4}>
|
|
62
|
+
{header && header}
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
</div>
|
|
64
|
+
{children && children}
|
|
64
65
|
|
|
65
66
|
{footer}
|
|
66
|
-
</
|
|
67
|
-
</
|
|
67
|
+
</Card>
|
|
68
|
+
</Container>
|
|
68
69
|
)
|
|
69
70
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {ThemeProvider} from '@sanity/ui'
|
|
3
|
+
import {buildTheme} from '@sanity/ui/theme'
|
|
4
|
+
import {render, type RenderResult} from '@testing-library/react'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
|
|
7
|
+
import {SanityProvider} from '../../context/SanityProvider'
|
|
8
|
+
|
|
9
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
10
|
+
const theme = buildTheme()
|
|
11
|
+
|
|
12
|
+
export const renderWithWrappers = (ui: React.ReactElement): RenderResult => {
|
|
13
|
+
return render(
|
|
14
|
+
<SanityProvider sanityInstance={sanityInstance}>
|
|
15
|
+
<ThemeProvider theme={theme}>{ui}</ThemeProvider>
|
|
16
|
+
</SanityProvider>,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -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
|
|
@@ -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)
|