@sanity/sdk-react 0.0.0-alpha.21 → 0.0.0-alpha.23
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 +502 -3460
- package/dist/index.js +400 -465
- package/dist/index.js.map +1 -1
- package/package.json +17 -15
- package/src/_exports/index.ts +4 -5
- package/src/components/SDKProvider.test.tsx +78 -54
- package/src/components/SDKProvider.tsx +31 -26
- package/src/components/SanityApp.test.tsx +121 -15
- package/src/components/SanityApp.tsx +26 -15
- package/src/components/auth/AuthBoundary.test.tsx +32 -14
- package/src/components/auth/AuthBoundary.tsx +53 -23
- package/src/components/auth/LoginCallback.test.tsx +19 -6
- package/src/components/auth/LoginCallback.tsx +2 -11
- package/src/components/auth/LoginError.test.tsx +12 -4
- package/src/components/auth/LoginError.tsx +13 -21
- package/src/components/auth/LoginFooter.test.tsx +7 -3
- package/src/context/ResourceProvider.test.tsx +157 -0
- package/src/context/ResourceProvider.tsx +111 -0
- package/src/context/SanityInstanceContext.ts +1 -1
- package/src/hooks/auth/useLoginUrl.tsx +14 -0
- package/src/hooks/client/useClient.ts +2 -1
- package/src/hooks/comlink/useManageFavorite.test.ts +16 -8
- package/src/hooks/comlink/useManageFavorite.ts +37 -13
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +8 -4
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +10 -8
- package/src/hooks/context/useSanityInstance.test.tsx +157 -15
- package/src/hooks/context/useSanityInstance.ts +66 -26
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +13 -31
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +12 -15
- package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.test.tsx → useStudioWorkspacesByProjectIdDataset.test.tsx} +13 -13
- package/src/hooks/dashboard/{useStudioWorkspacesByResourceId.ts → useStudioWorkspacesByProjectIdDataset.ts} +10 -9
- package/src/hooks/datasets/useDatasets.ts +15 -4
- package/src/hooks/document/useApplyDocumentActions.test.ts +4 -9
- package/src/hooks/document/useApplyDocumentActions.ts +6 -31
- package/src/hooks/document/useDocument.test.ts +2 -2
- package/src/hooks/document/useDocument.ts +40 -19
- package/src/hooks/document/useDocumentEvent.test.ts +2 -3
- package/src/hooks/document/useDocumentEvent.ts +7 -11
- package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
- package/src/hooks/document/useDocumentPermissions.ts +31 -23
- package/src/hooks/document/useDocumentSyncStatus.ts +5 -4
- package/src/hooks/document/useEditDocument.test.ts +2 -3
- package/src/hooks/document/useEditDocument.ts +43 -29
- package/src/hooks/documents/useDocuments.test.tsx +30 -3
- package/src/hooks/documents/useDocuments.ts +20 -7
- package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
- package/src/hooks/helpers/createCallbackHook.tsx +2 -3
- package/src/hooks/helpers/createStateSourceHook.test.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +5 -8
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +43 -18
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +36 -50
- package/src/hooks/preview/usePreview.test.tsx +66 -7
- package/src/hooks/preview/usePreview.tsx +17 -12
- package/src/hooks/projection/useProjection.test.tsx +68 -3
- package/src/hooks/projection/useProjection.ts +21 -24
- package/src/hooks/projects/useProject.ts +7 -4
- package/src/hooks/query/useQuery.ts +32 -14
- package/src/hooks/users/useUsers.test.tsx +330 -0
- package/src/hooks/users/useUsers.ts +65 -52
- package/src/components/Login/LoginLinks.test.tsx +0 -90
- package/src/components/Login/LoginLinks.tsx +0 -58
- package/src/components/auth/Login.test.tsx +0 -27
- package/src/components/auth/Login.tsx +0 -39
- package/src/components/auth/LoginLayout.test.tsx +0 -19
- package/src/components/auth/LoginLayout.tsx +0 -69
- package/src/components/auth/authTestHelpers.tsx +0 -11
- package/src/context/SanityProvider.test.tsx +0 -25
- package/src/context/SanityProvider.tsx +0 -50
- package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
- package/src/hooks/auth/useLoginUrls.tsx +0 -52
- package/src/hooks/users/useUsers.test.ts +0 -163
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
getUsersKey,
|
|
3
|
+
type GetUsersOptions,
|
|
4
|
+
getUsersState,
|
|
5
|
+
loadMoreUsers,
|
|
6
|
+
parseUsersKey,
|
|
7
|
+
resolveUsers,
|
|
8
|
+
type SanityUser,
|
|
9
|
+
} from '@sanity/sdk'
|
|
10
|
+
import {useCallback, useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
|
|
3
11
|
|
|
4
12
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
13
|
|
|
@@ -7,34 +15,20 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
7
15
|
* @public
|
|
8
16
|
* @category Types
|
|
9
17
|
*/
|
|
10
|
-
export interface
|
|
11
|
-
/**
|
|
12
|
-
* The type of resource to fetch users for.
|
|
13
|
-
*/
|
|
14
|
-
resourceType: ResourceType
|
|
15
|
-
/**
|
|
16
|
-
* The ID of the resource to fetch users for.
|
|
17
|
-
*/
|
|
18
|
-
resourceId: string
|
|
19
|
-
/**
|
|
20
|
-
* The limit of users to fetch.
|
|
21
|
-
*/
|
|
22
|
-
limit?: number
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @public
|
|
27
|
-
* @category Types
|
|
28
|
-
*/
|
|
29
|
-
export interface UseUsersResult {
|
|
18
|
+
export interface UsersResult {
|
|
30
19
|
/**
|
|
31
20
|
* The users fetched.
|
|
32
21
|
*/
|
|
33
|
-
|
|
22
|
+
data: SanityUser[]
|
|
34
23
|
/**
|
|
35
24
|
* Whether there are more users to fetch.
|
|
36
25
|
*/
|
|
37
26
|
hasMore: boolean
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether a users request is currently in progress
|
|
30
|
+
*/
|
|
31
|
+
isPending: boolean
|
|
38
32
|
/**
|
|
39
33
|
* Load more users.
|
|
40
34
|
*/
|
|
@@ -48,60 +42,79 @@ export interface UseUsersResult {
|
|
|
48
42
|
* Retrieves the users for a given resource (either a project or an organization).
|
|
49
43
|
*
|
|
50
44
|
* @category Users
|
|
51
|
-
* @param params - The resource type
|
|
45
|
+
* @param params - The resource type, project ID, and the limit of users to fetch
|
|
52
46
|
* @returns A list of users, a boolean indicating whether there are more users to fetch, and a function to load more users
|
|
53
47
|
*
|
|
54
48
|
* @example
|
|
55
49
|
* ```
|
|
56
|
-
* const {
|
|
50
|
+
* const { data, hasMore, loadMore, isPending } = useUsers({
|
|
57
51
|
* resourceType: 'organization',
|
|
58
|
-
*
|
|
59
|
-
*
|
|
52
|
+
* organizationId: 'my-org-id',
|
|
53
|
+
* batchSize: 10,
|
|
60
54
|
* })
|
|
61
55
|
*
|
|
62
56
|
* return (
|
|
63
57
|
* <div>
|
|
64
|
-
* {
|
|
58
|
+
* {data.map(user => (
|
|
65
59
|
* <figure key={user.sanityUserId}>
|
|
66
60
|
* <img src={user.profile.imageUrl} alt='' />
|
|
67
61
|
* <figcaption>{user.profile.displayName}</figcaption>
|
|
68
62
|
* <address>{user.profile.email}</address>
|
|
69
63
|
* </figure>
|
|
70
64
|
* ))}
|
|
71
|
-
* {hasMore && <button onClick={loadMore}>Load More</button>}
|
|
65
|
+
* {hasMore && <button onClick={loadMore}>{isPending ? 'Loading...' : 'Load More'</button>}
|
|
72
66
|
* </div>
|
|
73
67
|
* )
|
|
74
68
|
* ```
|
|
75
69
|
*/
|
|
76
|
-
export function useUsers(
|
|
77
|
-
const instance = useSanityInstance(
|
|
78
|
-
|
|
70
|
+
export function useUsers(options?: GetUsersOptions): UsersResult {
|
|
71
|
+
const instance = useSanityInstance(options)
|
|
72
|
+
// Use React's useTransition to avoid UI jank when user options change
|
|
73
|
+
const [isPending, startTransition] = useTransition()
|
|
74
|
+
|
|
75
|
+
// Get the unique key for this users request and its options
|
|
76
|
+
const key = getUsersKey(instance, options)
|
|
77
|
+
// Use a deferred state to avoid immediate re-renders when the users request changes
|
|
78
|
+
const [deferredKey, setDeferredKey] = useState(key)
|
|
79
|
+
// Parse the deferred users key back into users options
|
|
80
|
+
const deferred = useMemo(() => parseUsersKey(deferredKey), [deferredKey])
|
|
81
|
+
|
|
82
|
+
// Create an AbortController to cancel in-flight requests when needed
|
|
83
|
+
const [ref, setRef] = useState<AbortController>(new AbortController())
|
|
79
84
|
|
|
85
|
+
// When the users request or options change, start a transition to update the request
|
|
80
86
|
useEffect(() => {
|
|
81
|
-
|
|
82
|
-
resourceType: params.resourceType,
|
|
83
|
-
resourceId: params.resourceId,
|
|
84
|
-
})
|
|
85
|
-
}, [params.resourceType, params.resourceId, store])
|
|
87
|
+
if (key === deferredKey) return
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
startTransition(() => {
|
|
90
|
+
if (!ref.signal.aborted) {
|
|
91
|
+
ref.abort()
|
|
92
|
+
setRef(new AbortController())
|
|
91
93
|
}
|
|
92
|
-
const unsubscribe = store.getState().subscribe(onStoreChanged)
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
setDeferredKey(key)
|
|
96
|
+
})
|
|
97
|
+
}, [deferredKey, key, ref])
|
|
98
|
+
|
|
99
|
+
// Get the state source for this users request from the users store
|
|
100
|
+
const {getCurrent, subscribe} = useMemo(() => {
|
|
101
|
+
return getUsersState(instance, deferred)
|
|
102
|
+
}, [instance, deferred])
|
|
103
|
+
|
|
104
|
+
// If data isn't available yet, suspend rendering until it is
|
|
105
|
+
// This is the React Suspense integration - throwing a promise
|
|
106
|
+
// will cause React to show the nearest Suspense fallback
|
|
107
|
+
if (getCurrent() === undefined) {
|
|
108
|
+
throw resolveUsers(instance, {...deferred, signal: ref.signal})
|
|
109
|
+
}
|
|
101
110
|
|
|
102
|
-
|
|
111
|
+
// Subscribe to updates and get the current data
|
|
112
|
+
// useSyncExternalStore ensures the component re-renders when the data changes
|
|
113
|
+
const {data, hasMore} = useSyncExternalStore(subscribe, getCurrent)!
|
|
103
114
|
|
|
104
|
-
const
|
|
115
|
+
const loadMore = useCallback(() => {
|
|
116
|
+
loadMoreUsers(instance, options)
|
|
117
|
+
}, [instance, options])
|
|
105
118
|
|
|
106
|
-
return {
|
|
119
|
+
return {data, hasMore, isPending, loadMore}
|
|
107
120
|
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import {AuthStateType, createSanityInstance} from '@sanity/sdk'
|
|
2
|
-
import {SanityProvider, useAuthState, useLoginUrls} from '@sanity/sdk-react'
|
|
3
|
-
import {render, screen} from '@testing-library/react'
|
|
4
|
-
import React from 'react'
|
|
5
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
6
|
-
|
|
7
|
-
import {LoginLinks} from './LoginLinks'
|
|
8
|
-
|
|
9
|
-
// Mock the hooks and SDK functions
|
|
10
|
-
vi.mock('../../hooks/auth/useLoginUrls', () => ({
|
|
11
|
-
useLoginUrls: vi.fn(() => [
|
|
12
|
-
{
|
|
13
|
-
name: 'google',
|
|
14
|
-
title: 'Google',
|
|
15
|
-
url: 'https://google.com/auth',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
name: 'github',
|
|
19
|
-
title: 'GitHub',
|
|
20
|
-
url: 'https://github.com/auth',
|
|
21
|
-
},
|
|
22
|
-
]),
|
|
23
|
-
}))
|
|
24
|
-
vi.mock('@sanity/sdk', async () => {
|
|
25
|
-
const actual = await vi.importActual('@sanity/sdk')
|
|
26
|
-
|
|
27
|
-
return {
|
|
28
|
-
...actual,
|
|
29
|
-
tradeTokenForSession: vi.fn(),
|
|
30
|
-
getSidUrlHash: vi.fn().mockReturnValue(null),
|
|
31
|
-
getSidUrlSearch: vi.fn(),
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
vi.mock('../../hooks/auth/useAuthState', () => ({
|
|
36
|
-
useAuthState: vi.fn(() => 'logged-out'),
|
|
37
|
-
}))
|
|
38
|
-
|
|
39
|
-
vi.mock('../../hooks/auth/useHandleAuthCallback', () => ({
|
|
40
|
-
useHandleAuthCallback: vi.fn(),
|
|
41
|
-
}))
|
|
42
|
-
|
|
43
|
-
describe('LoginLinks', () => {
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
vi.clearAllMocks()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
49
|
-
const renderWithWrappers = (ui: React.ReactElement) => {
|
|
50
|
-
return render(<SanityProvider sanityInstances={[sanityInstance]}>{ui}</SanityProvider>)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
it('renders auth provider links correctly when not authenticated', () => {
|
|
54
|
-
vi.mocked(useAuthState).mockReturnValue({
|
|
55
|
-
type: AuthStateType.LOGGED_OUT,
|
|
56
|
-
isDestroyingSession: false,
|
|
57
|
-
})
|
|
58
|
-
renderWithWrappers(<LoginLinks />)
|
|
59
|
-
|
|
60
|
-
expect(screen.getByText('Choose login provider')).toBeInTheDocument()
|
|
61
|
-
|
|
62
|
-
const authProviders = useLoginUrls()
|
|
63
|
-
authProviders.forEach((provider) => {
|
|
64
|
-
const button = screen.getByRole('link', {name: provider.title})
|
|
65
|
-
expect(button).toBeInTheDocument()
|
|
66
|
-
expect(button).toHaveAttribute('href', provider.url)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('shows loading state while logging in', () => {
|
|
71
|
-
vi.mocked(useAuthState).mockReturnValue({
|
|
72
|
-
type: AuthStateType.LOGGING_IN,
|
|
73
|
-
isExchangingToken: false,
|
|
74
|
-
})
|
|
75
|
-
renderWithWrappers(<LoginLinks />)
|
|
76
|
-
|
|
77
|
-
expect(screen.getByText('Logging in...')).toBeInTheDocument()
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('shows success message when logged in', () => {
|
|
81
|
-
vi.mocked(useAuthState).mockReturnValue({
|
|
82
|
-
type: AuthStateType.LOGGED_IN,
|
|
83
|
-
token: 'test-token',
|
|
84
|
-
currentUser: null,
|
|
85
|
-
})
|
|
86
|
-
renderWithWrappers(<LoginLinks />)
|
|
87
|
-
|
|
88
|
-
expect(screen.getByText('You are logged in')).toBeInTheDocument()
|
|
89
|
-
})
|
|
90
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import {type ReactElement} from 'react'
|
|
2
|
-
|
|
3
|
-
import {useAuthState} from '../../hooks/auth/useAuthState'
|
|
4
|
-
import {useHandleAuthCallback} from '../../hooks/auth/useHandleAuthCallback'
|
|
5
|
-
import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Component that handles Sanity authentication flow and renders login provider options
|
|
9
|
-
*
|
|
10
|
-
* @public
|
|
11
|
-
*
|
|
12
|
-
* @returns Rendered component
|
|
13
|
-
*
|
|
14
|
-
* @remarks
|
|
15
|
-
* The component handles three states:
|
|
16
|
-
* 1. Loading state during token exchange
|
|
17
|
-
* 2. Success state after successful authentication
|
|
18
|
-
* 3. Provider selection UI when not authenticated
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* ```tsx
|
|
22
|
-
* const config = { projectId: 'your-project-id', dataset: 'production' }
|
|
23
|
-
* return <LoginLinks sanityInstance={config} />
|
|
24
|
-
* ```
|
|
25
|
-
*/
|
|
26
|
-
export const LoginLinks = (): ReactElement => {
|
|
27
|
-
const loginUrls = useLoginUrls()
|
|
28
|
-
const authState = useAuthState()
|
|
29
|
-
useHandleAuthCallback()
|
|
30
|
-
|
|
31
|
-
if (authState.type === 'logging-in') {
|
|
32
|
-
return <div className="sc-login-links__logging-in">Logging in...</div>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Show success state after authentication
|
|
36
|
-
if (authState.type === 'logged-in') {
|
|
37
|
-
return <div className="sc-login-links__logged-in">You are logged in</div>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Render provider selection UI
|
|
42
|
-
*/
|
|
43
|
-
return (
|
|
44
|
-
<div className="sc-login-links">
|
|
45
|
-
<h2 className="sc-login-links__title">Choose login provider</h2>
|
|
46
|
-
|
|
47
|
-
<ul className="sc-login-links__list">
|
|
48
|
-
{loginUrls.map((provider, index) => (
|
|
49
|
-
<li key={`${provider.url}_${index}`} className="sc-login-links__item">
|
|
50
|
-
<a href={provider.url} className="sc-login-links__link">
|
|
51
|
-
{provider.title}
|
|
52
|
-
</a>
|
|
53
|
-
</li>
|
|
54
|
-
))}
|
|
55
|
-
</ul>
|
|
56
|
-
</div>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import {screen} from '@testing-library/react'
|
|
2
|
-
import {describe, expect, it, vi} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {renderWithWrappers} from './authTestHelpers'
|
|
5
|
-
import {Login} from './Login'
|
|
6
|
-
|
|
7
|
-
vi.mock('../../hooks/auth/useLoginUrls', () => ({
|
|
8
|
-
useLoginUrls: vi.fn(() => [
|
|
9
|
-
{title: 'Provider A', url: 'https://provider-a.com/auth'},
|
|
10
|
-
{title: 'Provider B', url: 'https://provider-b.com/auth'},
|
|
11
|
-
]),
|
|
12
|
-
}))
|
|
13
|
-
|
|
14
|
-
describe('Login', () => {
|
|
15
|
-
it('renders login providers', () => {
|
|
16
|
-
renderWithWrappers(<Login />)
|
|
17
|
-
expect(screen.getByText('Choose login provider')).toBeInTheDocument()
|
|
18
|
-
expect(screen.getByRole('link', {name: 'Provider A'})).toHaveAttribute(
|
|
19
|
-
'href',
|
|
20
|
-
'https://provider-a.com/auth',
|
|
21
|
-
)
|
|
22
|
-
expect(screen.getByRole('link', {name: 'Provider B'})).toHaveAttribute(
|
|
23
|
-
'href',
|
|
24
|
-
'https://provider-b.com/auth',
|
|
25
|
-
)
|
|
26
|
-
})
|
|
27
|
-
})
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import {type JSX, Suspense} from 'react'
|
|
2
|
-
|
|
3
|
-
import {useLoginUrls} from '../../hooks/auth/useLoginUrls'
|
|
4
|
-
import {LoginLayout, type LoginLayoutProps} from './LoginLayout'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Login component that displays available authentication providers.
|
|
8
|
-
* Renders a list of login options with a loading fallback while providers load.
|
|
9
|
-
*
|
|
10
|
-
* @alpha
|
|
11
|
-
* @internal
|
|
12
|
-
*/
|
|
13
|
-
export function Login({header, footer}: LoginLayoutProps): JSX.Element {
|
|
14
|
-
return (
|
|
15
|
-
<LoginLayout header={header} footer={footer}>
|
|
16
|
-
<div className="sc-login">
|
|
17
|
-
<h1 className="sc-login__title">Choose login provider</h1>
|
|
18
|
-
|
|
19
|
-
<Suspense fallback={<div className="sc-login__loading">Loading…</div>}>
|
|
20
|
-
<Providers />
|
|
21
|
-
</Suspense>
|
|
22
|
-
</div>
|
|
23
|
-
</LoginLayout>
|
|
24
|
-
)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function Providers() {
|
|
28
|
-
const loginUrls = useLoginUrls()
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<div className="sc-login-providers">
|
|
32
|
-
{loginUrls.map(({title, url}) => (
|
|
33
|
-
<a key={url} href={url}>
|
|
34
|
-
{title}
|
|
35
|
-
</a>
|
|
36
|
-
))}
|
|
37
|
-
</div>
|
|
38
|
-
)
|
|
39
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import {screen} from '@testing-library/react'
|
|
2
|
-
import {describe, expect, it} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {renderWithWrappers} from './authTestHelpers'
|
|
5
|
-
import {LoginLayout} from './LoginLayout'
|
|
6
|
-
|
|
7
|
-
describe('LoginLayout', () => {
|
|
8
|
-
it('renders header, children, and footer', () => {
|
|
9
|
-
renderWithWrappers(
|
|
10
|
-
<LoginLayout header={<div>Header Content</div>} footer={<div>Footer Content</div>}>
|
|
11
|
-
<div>Main Content</div>
|
|
12
|
-
</LoginLayout>,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
expect(screen.getByText('Header Content')).toBeInTheDocument()
|
|
16
|
-
expect(screen.getByText('Main Content')).toBeInTheDocument()
|
|
17
|
-
expect(screen.getByText('Footer Content')).toBeInTheDocument()
|
|
18
|
-
})
|
|
19
|
-
})
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import {LoginFooter} from './LoginFooter'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @alpha
|
|
5
|
-
*/
|
|
6
|
-
export interface LoginLayoutProps {
|
|
7
|
-
/** Optional header content rendered at top of card */
|
|
8
|
-
header?: React.ReactNode
|
|
9
|
-
|
|
10
|
-
/** Optional footer content rendered below card. Defaults to an internal login footer */
|
|
11
|
-
footer?: React.ReactNode
|
|
12
|
-
|
|
13
|
-
/** Main content rendered in card body */
|
|
14
|
-
children?: React.ReactNode
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Layout component for login-related screens providing consistent styling and structure.
|
|
19
|
-
* Renders content in a centered card with optional header and footer sections.
|
|
20
|
-
*
|
|
21
|
-
* Can be used to build custom login screens for the AuthBoundary component, including:
|
|
22
|
-
* - Login provider selection (LoginComponent)
|
|
23
|
-
* - OAuth callback handling (CallbackComponent)
|
|
24
|
-
* - Error states (LoginErrorComponent)
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```tsx
|
|
28
|
-
* // Custom login screen using the layout
|
|
29
|
-
* function CustomLogin({header, footer}: LoginLayoutProps) {
|
|
30
|
-
* return (
|
|
31
|
-
* <LoginLayout
|
|
32
|
-
* header={header}
|
|
33
|
-
* footer={footer}
|
|
34
|
-
* >
|
|
35
|
-
* <CustomLoginContent />
|
|
36
|
-
* </LoginLayout>
|
|
37
|
-
* )
|
|
38
|
-
* }
|
|
39
|
-
*
|
|
40
|
-
* // Use with AuthBoundary
|
|
41
|
-
* <AuthBoundary
|
|
42
|
-
* LoginComponent={CustomLogin}
|
|
43
|
-
* header={<Logo />}
|
|
44
|
-
* >
|
|
45
|
-
* <ProtectedContent />
|
|
46
|
-
* </AuthBoundary>
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* @alpha
|
|
50
|
-
*/
|
|
51
|
-
export function LoginLayout({
|
|
52
|
-
children,
|
|
53
|
-
footer = <LoginFooter />,
|
|
54
|
-
header,
|
|
55
|
-
}: LoginLayoutProps): React.ReactNode {
|
|
56
|
-
return (
|
|
57
|
-
<div className="sc-login-layout">
|
|
58
|
-
<div className="sc-login-layout__container">
|
|
59
|
-
<div className="sc-login-layout__card">
|
|
60
|
-
{header && <div className="sc-login-layout__card-header">{header}</div>}
|
|
61
|
-
|
|
62
|
-
{children && <div className="sc-login-layout__card-body">{children}</div>}
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
{footer}
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
)
|
|
69
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
-
import {render, type RenderResult} from '@testing-library/react'
|
|
3
|
-
import React from 'react'
|
|
4
|
-
|
|
5
|
-
import {SanityProvider} from '../../context/SanityProvider'
|
|
6
|
-
|
|
7
|
-
const sanityInstance = createSanityInstance({projectId: 'test-project-id', dataset: 'production'})
|
|
8
|
-
|
|
9
|
-
export const renderWithWrappers = (ui: React.ReactElement): RenderResult => {
|
|
10
|
-
return render(<SanityProvider sanityInstances={[sanityInstance]}>{ui}</SanityProvider>)
|
|
11
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
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 sanityInstances={[sanityInstance]}>
|
|
19
|
-
<TestComponent />
|
|
20
|
-
</SanityProvider>,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
expect(getByTestId('test')).toHaveTextContent('test-project')
|
|
24
|
-
})
|
|
25
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import {type SanityInstance} from '@sanity/sdk'
|
|
2
|
-
import {type ReactElement} from 'react'
|
|
3
|
-
|
|
4
|
-
import {SanityInstanceContext} from './SanityInstanceContext'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @internal
|
|
8
|
-
*/
|
|
9
|
-
export interface SanityProviderProps {
|
|
10
|
-
children: React.ReactNode
|
|
11
|
-
sanityInstances: SanityInstance[]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @internal
|
|
16
|
-
*
|
|
17
|
-
* Top-level context provider that provides access to the Sanity configuration instance.
|
|
18
|
-
* This must wrap any components making use of the Sanity SDK React hooks.
|
|
19
|
-
*
|
|
20
|
-
* @remarks In most cases, SanityApp should be used rather than SanityProvider directly; SanityApp bundles both SanityProvider and an authentication layer.
|
|
21
|
-
* @param props - Sanity project and dataset configuration
|
|
22
|
-
* @returns Rendered component
|
|
23
|
-
* @example
|
|
24
|
-
* ```tsx
|
|
25
|
-
* import {createSanityInstance} from '@sanity/sdk'
|
|
26
|
-
* import {SanityProvider} from '@sanity/sdk-react'
|
|
27
|
-
*
|
|
28
|
-
* import MyAppRoot from './Root'
|
|
29
|
-
*
|
|
30
|
-
* const sanityInstance = createSanityInstance({
|
|
31
|
-
* projectId: 'your-project-id',
|
|
32
|
-
* dataset: 'production',
|
|
33
|
-
* })
|
|
34
|
-
*
|
|
35
|
-
* export default function MyApp() {
|
|
36
|
-
* return (
|
|
37
|
-
* <SanityProvider sanityInstance={sanityInstance}>
|
|
38
|
-
* <MyAppRoot />
|
|
39
|
-
* </SanityProvider>
|
|
40
|
-
* )
|
|
41
|
-
* }
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
export const SanityProvider = ({children, sanityInstances}: SanityProviderProps): ReactElement => {
|
|
45
|
-
return (
|
|
46
|
-
<SanityInstanceContext.Provider value={sanityInstances}>
|
|
47
|
-
{children}
|
|
48
|
-
</SanityInstanceContext.Provider>
|
|
49
|
-
)
|
|
50
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import {createSanityInstance, fetchLoginUrls, getLoginUrlsState} from '@sanity/sdk'
|
|
2
|
-
import {renderHook} from '@testing-library/react'
|
|
3
|
-
import {act, Suspense} from 'react'
|
|
4
|
-
import {throwError} from 'rxjs'
|
|
5
|
-
import {describe, expect, it, vi} from 'vitest'
|
|
6
|
-
|
|
7
|
-
import {useLoginUrls} from './useLoginUrls'
|
|
8
|
-
|
|
9
|
-
vi.mock('../context/useSanityInstance', () => ({
|
|
10
|
-
useSanityInstance: vi.fn().mockReturnValue(createSanityInstance({projectId: 'p', dataset: 'd'})),
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
|
-
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
14
|
-
const actual = await importOriginal()
|
|
15
|
-
return {...actual, getLoginUrlsState: vi.fn(), fetchLoginUrls: vi.fn()}
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('useLoginUrls', () => {
|
|
19
|
-
it('should suspend by throwing `fetchLoginUrls` if `getLoginUrlsState().getCurrent()` is falsy', async () => {
|
|
20
|
-
const subscribe = vi.fn()
|
|
21
|
-
const getCurrent = vi.fn().mockReturnValue(undefined)
|
|
22
|
-
|
|
23
|
-
let resolve: () => void
|
|
24
|
-
const promise = new Promise<void>((thisResolve) => {
|
|
25
|
-
resolve = thisResolve
|
|
26
|
-
})
|
|
27
|
-
vi.mocked(fetchLoginUrls).mockReturnValue(
|
|
28
|
-
promise as unknown as ReturnType<typeof fetchLoginUrls>,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
vi.mocked(getLoginUrlsState).mockReturnValue({
|
|
32
|
-
getCurrent,
|
|
33
|
-
subscribe,
|
|
34
|
-
observable: throwError(() => new Error('Unexpected usage of observable')),
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
38
|
-
<Suspense fallback={<>Loading…</>}>{children}</Suspense>
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
const {result, rerender} = renderHook(() => useLoginUrls(), {wrapper})
|
|
42
|
-
expect(result.current).toEqual(null)
|
|
43
|
-
|
|
44
|
-
const mockProviders = [{name: 'google', title: 'Google', url: 'http://test.com/auth/google'}]
|
|
45
|
-
|
|
46
|
-
await act(async () => {
|
|
47
|
-
getCurrent.mockReturnValue(mockProviders)
|
|
48
|
-
resolve()
|
|
49
|
-
rerender()
|
|
50
|
-
await promise
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
expect(result.current).toEqual(mockProviders)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('should render according to `getLoginUrlsState`', () => {
|
|
57
|
-
const subscribe = vi.fn()
|
|
58
|
-
const mockProviders = [{name: 'google', title: 'Google', url: 'http://test.com/auth/google'}]
|
|
59
|
-
vi.mocked(getLoginUrlsState).mockReturnValue({
|
|
60
|
-
getCurrent: () => mockProviders,
|
|
61
|
-
subscribe,
|
|
62
|
-
observable: throwError(() => new Error('Unexpected usage of observable')),
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
const {result} = renderHook(() => useLoginUrls())
|
|
66
|
-
expect(result.current).toEqual(mockProviders)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import {type AuthProvider, fetchLoginUrls, getLoginUrlsState} from '@sanity/sdk'
|
|
2
|
-
import {useMemo, useSyncExternalStore} from 'react'
|
|
3
|
-
|
|
4
|
-
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @internal
|
|
8
|
-
* A React hook that retrieves the available authentication provider URLs for login.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* This hook fetches the login URLs from the Sanity auth store when the component mounts.
|
|
12
|
-
* Each provider object contains information about an authentication method, including its URL.
|
|
13
|
-
* The hook will suspend if the login URLs have not yet loaded.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```tsx
|
|
17
|
-
* // LoginProviders component that uses the hook
|
|
18
|
-
* function LoginProviders() {
|
|
19
|
-
* const providers = useLoginUrls()
|
|
20
|
-
*
|
|
21
|
-
* return (
|
|
22
|
-
* <div>
|
|
23
|
-
* {providers.map((provider) => (
|
|
24
|
-
* <a key={provider.name} href={provider.url}>
|
|
25
|
-
* Login with {provider.title}
|
|
26
|
-
* </a>
|
|
27
|
-
* ))}
|
|
28
|
-
* </div>
|
|
29
|
-
* )
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* // Parent component with Suspense boundary
|
|
33
|
-
* function LoginPage() {
|
|
34
|
-
* return (
|
|
35
|
-
* <Suspense fallback={<div>Loading authentication providers...</div>}>
|
|
36
|
-
* <LoginProviders />
|
|
37
|
-
* </Suspense>
|
|
38
|
-
* )
|
|
39
|
-
* }
|
|
40
|
-
* ```
|
|
41
|
-
*
|
|
42
|
-
* @returns An array of {@link AuthProvider} objects containing login URLs and provider information
|
|
43
|
-
* @public
|
|
44
|
-
*/
|
|
45
|
-
export function useLoginUrls(): AuthProvider[] {
|
|
46
|
-
const instance = useSanityInstance()
|
|
47
|
-
const {subscribe, getCurrent} = useMemo(() => getLoginUrlsState(instance), [instance])
|
|
48
|
-
|
|
49
|
-
if (!getCurrent()) throw fetchLoginUrls(instance)
|
|
50
|
-
|
|
51
|
-
return useSyncExternalStore(subscribe, getCurrent as () => AuthProvider[])
|
|
52
|
-
}
|