@sanity/sdk-react 0.0.0-alpha.1
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/_chunks-es/useLogOut.js +36 -0
- package/dist/_chunks-es/useLogOut.js.map +1 -0
- package/dist/components.d.ts +235 -0
- package/dist/components.js +250 -0
- package/dist/components.js.map +1 -0
- package/dist/hooks.d.ts +145 -0
- package/dist/hooks.js +27 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/package.json +113 -0
- package/src/_exports/components.ts +12 -0
- package/src/_exports/hooks.ts +7 -0
- package/src/_exports/index.ts +10 -0
- package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +95 -0
- package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +42 -0
- package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +23 -0
- package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +95 -0
- package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +42 -0
- package/src/components/DocumentListLayout/DocumentListLayout.tsx +15 -0
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +49 -0
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +34 -0
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +30 -0
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +115 -0
- package/src/components/Login/LoginLinks.test.tsx +100 -0
- package/src/components/Login/LoginLinks.tsx +73 -0
- package/src/components/auth/AuthBoundary.test.tsx +103 -0
- package/src/components/auth/AuthBoundary.tsx +101 -0
- package/src/components/auth/AuthError.test.ts +36 -0
- package/src/components/auth/AuthError.ts +27 -0
- package/src/components/auth/Login.test.tsx +41 -0
- package/src/components/auth/Login.tsx +58 -0
- package/src/components/auth/LoginCallback.test.tsx +86 -0
- package/src/components/auth/LoginCallback.tsx +41 -0
- package/src/components/auth/LoginError.test.tsx +56 -0
- package/src/components/auth/LoginError.tsx +54 -0
- package/src/components/auth/LoginFooter.test.tsx +29 -0
- package/src/components/auth/LoginFooter.tsx +67 -0
- package/src/components/auth/LoginLayout.test.tsx +33 -0
- package/src/components/auth/LoginLayout.tsx +99 -0
- package/src/components/context/SanityProvider.test.tsx +25 -0
- package/src/components/context/SanityProvider.tsx +42 -0
- package/src/hooks/Documents/.keep +0 -0
- package/src/hooks/auth/useAuthState.test.tsx +106 -0
- package/src/hooks/auth/useAuthState.tsx +33 -0
- package/src/hooks/auth/useAuthToken.test.tsx +94 -0
- package/src/hooks/auth/useAuthToken.tsx +16 -0
- package/src/hooks/auth/useCurrentUser.test.tsx +50 -0
- package/src/hooks/auth/useCurrentUser.tsx +27 -0
- package/src/hooks/auth/useHandleCallback.test.tsx +25 -0
- package/src/hooks/auth/useHandleCallback.tsx +50 -0
- package/src/hooks/auth/useLogOut.test.tsx +67 -0
- package/src/hooks/auth/useLogOut.tsx +15 -0
- package/src/hooks/auth/useLoginUrls.test.tsx +61 -0
- package/src/hooks/auth/useLoginUrls.tsx +51 -0
- package/src/hooks/client/useClient.test.tsx +130 -0
- package/src/hooks/client/useClient.ts +56 -0
- package/src/hooks/context/useSanityInstance.test.tsx +31 -0
- package/src/hooks/context/useSanityInstance.ts +23 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {ThemeProvider} from '@sanity/ui'
|
|
3
|
+
import {buildTheme} from '@sanity/ui/theme'
|
|
4
|
+
import {render, screen} from '@testing-library/react'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
7
|
+
|
|
8
|
+
import {SanityProvider} from '../context/SanityProvider'
|
|
9
|
+
import {Login} from './Login'
|
|
10
|
+
|
|
11
|
+
vi.mock('../../hooks/auth/useLoginUrls', () => ({
|
|
12
|
+
useLoginUrls: vi.fn(() => [
|
|
13
|
+
{title: 'Provider A', url: 'https://provider-a.com/auth'},
|
|
14
|
+
{title: 'Provider B', url: 'https://provider-b.com/auth'},
|
|
15
|
+
]),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
const theme = buildTheme({})
|
|
19
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
20
|
+
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
21
|
+
return render(
|
|
22
|
+
<ThemeProvider theme={theme}>
|
|
23
|
+
<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
|
|
24
|
+
</ThemeProvider>,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('Login', () => {
|
|
29
|
+
it('renders login providers', () => {
|
|
30
|
+
renderWithWrappers(<Login />)
|
|
31
|
+
expect(screen.getByText('Choose login provider')).toBeInTheDocument()
|
|
32
|
+
expect(screen.getByRole('link', {name: 'Provider A'})).toHaveAttribute(
|
|
33
|
+
'href',
|
|
34
|
+
'https://provider-a.com/auth',
|
|
35
|
+
)
|
|
36
|
+
expect(screen.getByRole('link', {name: 'Provider B'})).toHaveAttribute(
|
|
37
|
+
'href',
|
|
38
|
+
'https://provider-b.com/auth',
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import {Button, Flex, Heading, Spinner} from '@sanity/ui'
|
|
2
|
+
import {Suspense} from 'react'
|
|
3
|
+
import styled from 'styled-components'
|
|
4
|
+
|
|
5
|
+
import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
|
|
6
|
+
import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @alpha
|
|
10
|
+
*/
|
|
11
|
+
export interface LoginProps {
|
|
12
|
+
header?: React.ReactNode
|
|
13
|
+
footer?: React.ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const FallbackRoot = styled(Flex)`
|
|
17
|
+
height: 123px;
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Login component that displays available authentication providers.
|
|
22
|
+
* Renders a list of login options with a loading fallback while providers load.
|
|
23
|
+
*
|
|
24
|
+
* @alpha
|
|
25
|
+
*/
|
|
26
|
+
export function Login({header, footer}: LoginLayoutProps): JSX.Element {
|
|
27
|
+
return (
|
|
28
|
+
<LoginLayout header={header} footer={footer}>
|
|
29
|
+
<Flex direction="column" gap={4}>
|
|
30
|
+
<Heading as="h1" size={1} align="center">
|
|
31
|
+
Choose login provider
|
|
32
|
+
</Heading>
|
|
33
|
+
|
|
34
|
+
<Suspense
|
|
35
|
+
fallback={
|
|
36
|
+
<FallbackRoot align="center" justify="center">
|
|
37
|
+
<Spinner />
|
|
38
|
+
</FallbackRoot>
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
<Providers />
|
|
42
|
+
</Suspense>
|
|
43
|
+
</Flex>
|
|
44
|
+
</LoginLayout>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Providers() {
|
|
49
|
+
const loginUrls = useLoginUrls()
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Flex direction="column" gap={3}>
|
|
53
|
+
{loginUrls.map(({title, url}) => (
|
|
54
|
+
<Button key={url} text={title} as="a" href={url} mode="ghost" />
|
|
55
|
+
))}
|
|
56
|
+
</Flex>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {ThemeProvider} from '@sanity/ui'
|
|
3
|
+
import {buildTheme} from '@sanity/ui/theme'
|
|
4
|
+
import {render, screen, waitFor} from '@testing-library/react'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
7
|
+
|
|
8
|
+
import {SanityProvider} from '../context/SanityProvider'
|
|
9
|
+
|
|
10
|
+
const theme = buildTheme({})
|
|
11
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
12
|
+
|
|
13
|
+
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
14
|
+
return render(
|
|
15
|
+
<ThemeProvider theme={theme}>
|
|
16
|
+
<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
|
|
17
|
+
</ThemeProvider>,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Mock `useHandleCallback`
|
|
22
|
+
vi.mock('../../hooks/auth/useHandleCallback', () => ({
|
|
23
|
+
useHandleCallback: vi.fn(() => async (url: string) => {
|
|
24
|
+
const parsedUrl = new URL(url)
|
|
25
|
+
const sid = new URLSearchParams(parsedUrl.hash.slice(1)).get('sid')
|
|
26
|
+
if (sid === 'valid') {
|
|
27
|
+
return 'https://example.com/new-location'
|
|
28
|
+
}
|
|
29
|
+
return false
|
|
30
|
+
}),
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
describe('LoginCallback', () => {
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
// Stub `window.history` and `location`
|
|
36
|
+
vi.stubGlobal('history', {
|
|
37
|
+
replaceState: vi.fn(),
|
|
38
|
+
})
|
|
39
|
+
vi.stubGlobal('location', {
|
|
40
|
+
href: 'http://localhost',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
vi.unstubAllGlobals()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks()
|
|
50
|
+
vi.resetModules()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('renders a loading message', async () => {
|
|
54
|
+
const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
|
|
55
|
+
renderWithWrappers(<LoginCallback />)
|
|
56
|
+
expect(screen.getByText('Logging you in…')).toBeInTheDocument()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('handles a successful callback and calls history.replaceState', async () => {
|
|
60
|
+
// Simulate a valid `sid` in the location hash
|
|
61
|
+
vi.stubGlobal('location', {href: 'http://localhost#sid=valid'})
|
|
62
|
+
const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
|
|
63
|
+
|
|
64
|
+
renderWithWrappers(<LoginCallback />)
|
|
65
|
+
|
|
66
|
+
await waitFor(() => {
|
|
67
|
+
expect(history.replaceState).toHaveBeenCalledWith(
|
|
68
|
+
null,
|
|
69
|
+
'',
|
|
70
|
+
'https://example.com/new-location',
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('does not call history.replaceState on an unsuccessful callback', async () => {
|
|
76
|
+
// Simulate an invalid `sid` in the location hash
|
|
77
|
+
vi.stubGlobal('location', {href: 'http://localhost#sid=invalid'})
|
|
78
|
+
const {LoginCallback} = await import('./LoginCallback') // Reload after resetModules
|
|
79
|
+
|
|
80
|
+
renderWithWrappers(<LoginCallback />)
|
|
81
|
+
|
|
82
|
+
await waitFor(() => {
|
|
83
|
+
expect(history.replaceState).not.toHaveBeenCalled()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {Flex, Spinner, Text} from '@sanity/ui'
|
|
2
|
+
import {useEffect} from 'react'
|
|
3
|
+
import styled from 'styled-components'
|
|
4
|
+
|
|
5
|
+
import {useHandleCallback} from '../../hooks/auth/useHandleCallback'
|
|
6
|
+
import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
7
|
+
|
|
8
|
+
const StyledFlex = styled(Flex)`
|
|
9
|
+
margin: auto;
|
|
10
|
+
`
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Component shown during auth callback processing that handles login completion.
|
|
14
|
+
* Automatically processes the auth callback when mounted and updates the URL
|
|
15
|
+
* to remove callback parameters without triggering a page reload.
|
|
16
|
+
*
|
|
17
|
+
* @alpha
|
|
18
|
+
*/
|
|
19
|
+
export function LoginCallback({header, footer}: LoginLayoutProps): React.ReactNode {
|
|
20
|
+
const handleCallback = useHandleCallback()
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const url = new URL(location.href)
|
|
24
|
+
handleCallback(url.toString()).then((replacementLocation) => {
|
|
25
|
+
if (replacementLocation) {
|
|
26
|
+
// history API with `replaceState` is used to prevent a reload but still
|
|
27
|
+
// remove the short-lived token from the URL
|
|
28
|
+
history.replaceState(null, '', replacementLocation)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}, [handleCallback])
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<LoginLayout header={header} footer={footer}>
|
|
35
|
+
<StyledFlex direction="column" justify="center" align="center" gap={4}>
|
|
36
|
+
<Text size={1}>Logging you in…</Text>
|
|
37
|
+
<Spinner size={4} />
|
|
38
|
+
</StyledFlex>
|
|
39
|
+
</LoginLayout>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {ThemeProvider} from '@sanity/ui'
|
|
3
|
+
import {buildTheme} from '@sanity/ui/theme'
|
|
4
|
+
import {fireEvent, render, screen, waitFor} from '@testing-library/react'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
7
|
+
|
|
8
|
+
import {SanityProvider} from '../context/SanityProvider'
|
|
9
|
+
import {AuthError} from './AuthError'
|
|
10
|
+
import {LoginError} from './LoginError'
|
|
11
|
+
|
|
12
|
+
vi.mock('../../hooks/auth/useLogOut', () => ({
|
|
13
|
+
useLogOut: vi.fn(() => async () => {}),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const theme = buildTheme({})
|
|
17
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
18
|
+
|
|
19
|
+
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
20
|
+
return render(
|
|
21
|
+
<ThemeProvider theme={theme}>
|
|
22
|
+
<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
|
|
23
|
+
</ThemeProvider>,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('LoginError', () => {
|
|
28
|
+
it('shows authentication error and retry button', async () => {
|
|
29
|
+
const mockReset = vi.fn()
|
|
30
|
+
const error = new AuthError(new Error('Test error'))
|
|
31
|
+
|
|
32
|
+
renderWithWrappers(<LoginError error={error} resetErrorBoundary={mockReset} />)
|
|
33
|
+
|
|
34
|
+
expect(screen.getByText('Authentication Error')).toBeInTheDocument()
|
|
35
|
+
const retryButton = screen.getByRole('button', {name: 'Retry'})
|
|
36
|
+
fireEvent.click(retryButton)
|
|
37
|
+
|
|
38
|
+
await waitFor(() => {
|
|
39
|
+
expect(mockReset).toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('throws an error if the error is not an instance of AuthError', () => {
|
|
44
|
+
const mockReset = vi.fn()
|
|
45
|
+
const nonAuthError = new Error('Non-auth error')
|
|
46
|
+
|
|
47
|
+
// Suppress console.error during this test
|
|
48
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
49
|
+
|
|
50
|
+
expect(() => {
|
|
51
|
+
renderWithWrappers(<LoginError error={nonAuthError} resetErrorBoundary={mockReset} />)
|
|
52
|
+
}).toThrow('Non-auth error')
|
|
53
|
+
|
|
54
|
+
consoleErrorSpy.mockRestore() // Restore original console.error behavior
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {Button, Flex, Text} from '@sanity/ui'
|
|
2
|
+
import {useCallback} from 'react'
|
|
3
|
+
import {type FallbackProps} from 'react-error-boundary'
|
|
4
|
+
import styled from 'styled-components'
|
|
5
|
+
|
|
6
|
+
import {useLogOut} from '../../hooks/auth/useLogOut'
|
|
7
|
+
import {AuthError} from './AuthError'
|
|
8
|
+
import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @alpha
|
|
12
|
+
*/
|
|
13
|
+
export type LoginErrorProps = FallbackProps & LoginLayoutProps
|
|
14
|
+
|
|
15
|
+
const StyledFlex = styled(Flex)`
|
|
16
|
+
margin: auto;
|
|
17
|
+
`
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Displays authentication error details and provides retry functionality.
|
|
21
|
+
* Only handles {@link AuthError} instances - rethrows other error types.
|
|
22
|
+
*
|
|
23
|
+
* @alpha
|
|
24
|
+
*/
|
|
25
|
+
export function LoginError({
|
|
26
|
+
error,
|
|
27
|
+
resetErrorBoundary,
|
|
28
|
+
header,
|
|
29
|
+
footer,
|
|
30
|
+
}: LoginErrorProps): React.ReactNode {
|
|
31
|
+
if (!(error instanceof AuthError)) throw error
|
|
32
|
+
const logout = useLogOut()
|
|
33
|
+
|
|
34
|
+
const handleRetry = useCallback(async () => {
|
|
35
|
+
await logout()
|
|
36
|
+
resetErrorBoundary()
|
|
37
|
+
}, [logout, resetErrorBoundary])
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<LoginLayout header={header} footer={footer}>
|
|
41
|
+
<StyledFlex direction="column" gap={4}>
|
|
42
|
+
<Flex direction="column" gap={3}>
|
|
43
|
+
<Text as="h2" align="center" weight="bold" size={3}>
|
|
44
|
+
Authentication Error
|
|
45
|
+
</Text>
|
|
46
|
+
<Text size={1} align="center">
|
|
47
|
+
Please try again or contact support if the problem persists.
|
|
48
|
+
</Text>
|
|
49
|
+
</Flex>
|
|
50
|
+
<Button text="Retry" tone="primary" onClick={handleRetry} />
|
|
51
|
+
</StyledFlex>
|
|
52
|
+
</LoginLayout>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {ThemeProvider} from '@sanity/ui'
|
|
3
|
+
import {buildTheme} from '@sanity/ui/theme'
|
|
4
|
+
import {render, screen} from '@testing-library/react'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import {describe, expect, it} from 'vitest'
|
|
7
|
+
|
|
8
|
+
import {SanityProvider} from '../context/SanityProvider'
|
|
9
|
+
import {LoginFooter} from './LoginFooter'
|
|
10
|
+
|
|
11
|
+
const theme = buildTheme({})
|
|
12
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
13
|
+
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
14
|
+
return render(
|
|
15
|
+
<ThemeProvider theme={theme}>
|
|
16
|
+
<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
|
|
17
|
+
</ThemeProvider>,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('LoginFooter', () => {
|
|
22
|
+
it('renders footer links', () => {
|
|
23
|
+
renderWithWrappers(<LoginFooter />)
|
|
24
|
+
expect(screen.getByText('Community')).toBeInTheDocument()
|
|
25
|
+
expect(screen.getByText('Docs')).toBeInTheDocument()
|
|
26
|
+
expect(screen.getByText('Privacy')).toBeInTheDocument()
|
|
27
|
+
expect(screen.getByText('sanity.io')).toBeInTheDocument()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {SanityLogo} from '@sanity/logos'
|
|
2
|
+
import {Flex, Text} from '@sanity/ui'
|
|
3
|
+
import {Fragment} from 'react'
|
|
4
|
+
import styled from 'styled-components'
|
|
5
|
+
|
|
6
|
+
const LINKS = [
|
|
7
|
+
{
|
|
8
|
+
url: 'https://slack.sanity.io/',
|
|
9
|
+
i18nKey: 'workspaces.community-title',
|
|
10
|
+
title: 'Community',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
url: 'https://www.sanity.io/docs',
|
|
14
|
+
i18nKey: 'workspaces.docs-title',
|
|
15
|
+
title: 'Docs',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
url: 'https://www.sanity.io/legal/privacy',
|
|
19
|
+
i18nKey: 'workspaces.privacy-title',
|
|
20
|
+
title: 'Privacy',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
url: 'https://www.sanity.io',
|
|
24
|
+
i18nKey: 'workspaces.sanity-io-title',
|
|
25
|
+
title: 'sanity.io',
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const StyledText = styled(Text)`
|
|
30
|
+
a {
|
|
31
|
+
color: inherit;
|
|
32
|
+
}
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default footer component for login screens showing Sanity branding and legal
|
|
37
|
+
* links.
|
|
38
|
+
*
|
|
39
|
+
* @alpha
|
|
40
|
+
*/
|
|
41
|
+
export function LoginFooter(): React.ReactNode {
|
|
42
|
+
return (
|
|
43
|
+
<Flex direction="column" gap={4} justify="center" align="center" paddingTop={2}>
|
|
44
|
+
<Text size={3}>
|
|
45
|
+
<SanityLogo />
|
|
46
|
+
</Text>
|
|
47
|
+
|
|
48
|
+
<Flex align="center" gap={2}>
|
|
49
|
+
{LINKS.map((link, index) => (
|
|
50
|
+
<Fragment key={link.title}>
|
|
51
|
+
<StyledText muted size={1}>
|
|
52
|
+
<a href={link.url} target="_blank" rel="noopener noreferrer">
|
|
53
|
+
{link.title}
|
|
54
|
+
</a>
|
|
55
|
+
</StyledText>
|
|
56
|
+
|
|
57
|
+
{index < LINKS.length - 1 && (
|
|
58
|
+
<Text size={1} muted>
|
|
59
|
+
•
|
|
60
|
+
</Text>
|
|
61
|
+
)}
|
|
62
|
+
</Fragment>
|
|
63
|
+
))}
|
|
64
|
+
</Flex>
|
|
65
|
+
</Flex>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {ThemeProvider} from '@sanity/ui'
|
|
3
|
+
import {buildTheme} from '@sanity/ui/theme'
|
|
4
|
+
import {render, screen} from '@testing-library/react'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import {describe, expect, it} from 'vitest'
|
|
7
|
+
|
|
8
|
+
import {SanityProvider} from '../context/SanityProvider'
|
|
9
|
+
import {LoginLayout} from './LoginLayout'
|
|
10
|
+
|
|
11
|
+
const theme = buildTheme({})
|
|
12
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
13
|
+
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
14
|
+
return render(
|
|
15
|
+
<ThemeProvider theme={theme}>
|
|
16
|
+
<SanityProvider sanityInstance={sanityInstance}>{ui}</SanityProvider>
|
|
17
|
+
</ThemeProvider>,
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('LoginLayout', () => {
|
|
22
|
+
it('renders header, children, and footer', () => {
|
|
23
|
+
renderWithWrappers(
|
|
24
|
+
<LoginLayout header={<div>Header Content</div>} footer={<div>Footer Content</div>}>
|
|
25
|
+
<div>Main Content</div>
|
|
26
|
+
</LoginLayout>,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
expect(screen.getByText('Header Content')).toBeInTheDocument()
|
|
30
|
+
expect(screen.getByText('Main Content')).toBeInTheDocument()
|
|
31
|
+
expect(screen.getByText('Footer Content')).toBeInTheDocument()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {Card, Flex} from '@sanity/ui'
|
|
2
|
+
import styled from 'styled-components'
|
|
3
|
+
|
|
4
|
+
import {LoginFooter} from './LoginFooter'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @alpha
|
|
8
|
+
*/
|
|
9
|
+
export interface LoginLayoutProps {
|
|
10
|
+
/** Optional header content rendered at top of card */
|
|
11
|
+
header?: React.ReactNode
|
|
12
|
+
|
|
13
|
+
/** Optional footer content rendered below card. Defaults to an internal login footer */
|
|
14
|
+
footer?: React.ReactNode
|
|
15
|
+
|
|
16
|
+
/** Main content rendered in card body */
|
|
17
|
+
children?: React.ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Root = styled.div`
|
|
21
|
+
width: 100%;
|
|
22
|
+
display: flex;
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
const Container = styled(Flex)`
|
|
26
|
+
width: 320px;
|
|
27
|
+
margin: auto;
|
|
28
|
+
display: flex;
|
|
29
|
+
`
|
|
30
|
+
|
|
31
|
+
const StyledCard = styled(Card)``
|
|
32
|
+
|
|
33
|
+
const ChildrenFlex = styled(Flex)`
|
|
34
|
+
min-height: 154px;
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Layout component for login-related screens providing consistent styling and structure.
|
|
39
|
+
* Renders content in a centered card with optional header and footer sections.
|
|
40
|
+
*
|
|
41
|
+
* Can be used to build custom login screens for the AuthBoundary component, including:
|
|
42
|
+
* - Login provider selection (LoginComponent)
|
|
43
|
+
* - OAuth callback handling (CallbackComponent)
|
|
44
|
+
* - Error states (LoginErrorComponent)
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* // Custom login screen using the layout
|
|
49
|
+
* function CustomLogin({header, footer}: LoginLayoutProps) {
|
|
50
|
+
* return (
|
|
51
|
+
* <LoginLayout
|
|
52
|
+
* header={header}
|
|
53
|
+
* footer={footer}
|
|
54
|
+
* >
|
|
55
|
+
* <CustomLoginContent />
|
|
56
|
+
* </LoginLayout>
|
|
57
|
+
* )
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* // Use with AuthBoundary
|
|
61
|
+
* <AuthBoundary
|
|
62
|
+
* LoginComponent={CustomLogin}
|
|
63
|
+
* header={<Logo />}
|
|
64
|
+
* >
|
|
65
|
+
* <ProtectedContent />
|
|
66
|
+
* </AuthBoundary>
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @alpha
|
|
70
|
+
*/
|
|
71
|
+
export function LoginLayout({
|
|
72
|
+
children,
|
|
73
|
+
footer = <LoginFooter />,
|
|
74
|
+
header,
|
|
75
|
+
}: LoginLayoutProps): React.ReactNode {
|
|
76
|
+
return (
|
|
77
|
+
<Root>
|
|
78
|
+
<Container direction="column" gap={4}>
|
|
79
|
+
<StyledCard border radius={2} paddingY={4}>
|
|
80
|
+
<Flex direction="column" gap={4}>
|
|
81
|
+
{header && (
|
|
82
|
+
<Card borderBottom paddingX={4} paddingBottom={3}>
|
|
83
|
+
{header}
|
|
84
|
+
</Card>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{children && (
|
|
88
|
+
<ChildrenFlex paddingX={4} direction="column">
|
|
89
|
+
{children}
|
|
90
|
+
</ChildrenFlex>
|
|
91
|
+
)}
|
|
92
|
+
</Flex>
|
|
93
|
+
</StyledCard>
|
|
94
|
+
|
|
95
|
+
{footer}
|
|
96
|
+
</Container>
|
|
97
|
+
</Root>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {render} from '@testing-library/react'
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {useSanityInstance} from '../../hooks/context/useSanityInstance'
|
|
6
|
+
import {SanityProvider} from './SanityProvider'
|
|
7
|
+
|
|
8
|
+
describe('SanityProvider', () => {
|
|
9
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project', dataset: 'production'})
|
|
10
|
+
|
|
11
|
+
it('provides instance to nested components', () => {
|
|
12
|
+
const TestComponent = () => {
|
|
13
|
+
const instance = useSanityInstance()
|
|
14
|
+
return <div data-testid="test">{instance.identity.projectId}</div>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const {getByTestId} = render(
|
|
18
|
+
<SanityProvider sanityInstance={sanityInstance}>
|
|
19
|
+
<TestComponent />
|
|
20
|
+
</SanityProvider>,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
expect(getByTestId('test')).toHaveTextContent('test-project')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {createContext, type ReactElement} from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export interface SanityProviderProps {
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
sanityInstance: SanityInstance
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const SanityInstanceContext = createContext<SanityInstance | null>(null)
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Top-level context provider that provides a Sanity configuration instance.
|
|
16
|
+
* This must wrap any Sanity SDK React component.
|
|
17
|
+
* @public
|
|
18
|
+
* @param props - Sanity project and dataset configuration
|
|
19
|
+
* @returns Rendered component
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import {createSanityInstance} from '@sanity/sdk'
|
|
23
|
+
* import {ExampleComponent, SanityProvider} from '@sanity/sdk-react'
|
|
24
|
+
*
|
|
25
|
+
* const sanityInstance = createSanityInstance({projectId: 'your-project-id', dataset: 'production'})
|
|
26
|
+
*
|
|
27
|
+
* export default function MyApp() {
|
|
28
|
+
* return (
|
|
29
|
+
* <SanityProvider sanityInstance={sanityInstance}>
|
|
30
|
+
* <ExampleComponent />
|
|
31
|
+
* </SanityProvider>
|
|
32
|
+
* )
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const SanityProvider = ({children, sanityInstance}: SanityProviderProps): ReactElement => {
|
|
37
|
+
return (
|
|
38
|
+
<SanityInstanceContext.Provider value={sanityInstance}>
|
|
39
|
+
{children}
|
|
40
|
+
</SanityInstanceContext.Provider>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
File without changes
|