@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,130 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {act} from '@testing-library/react'
|
|
3
|
+
import type {Subscribable, Subscriber} from 'rxjs'
|
|
4
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {renderHook} from '../../../test/test-utils'
|
|
7
|
+
import {useClient} from './useClient'
|
|
8
|
+
|
|
9
|
+
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal()
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
getClient: vi.fn(),
|
|
14
|
+
getSubscribableClient: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const {getClient, getSubscribableClient} = await import('@sanity/sdk')
|
|
19
|
+
|
|
20
|
+
describe('useClient', () => {
|
|
21
|
+
let subscribers: {next: (client: SanityClient) => void}[] = []
|
|
22
|
+
let currentClient: SanityClient
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
subscribers = []
|
|
26
|
+
|
|
27
|
+
currentClient = {
|
|
28
|
+
config: () => ({token: undefined, apiVersion: 'v2024-11-12'}),
|
|
29
|
+
} as unknown as SanityClient
|
|
30
|
+
|
|
31
|
+
// Create a subscribable interface directly
|
|
32
|
+
const createSubscribable = (): Subscribable<SanityClient> => ({
|
|
33
|
+
subscribe: (subscriber: {next: (client: SanityClient) => void}) => {
|
|
34
|
+
subscribers.push(subscriber)
|
|
35
|
+
subscriber.next(currentClient)
|
|
36
|
+
return {
|
|
37
|
+
unsubscribe: () => {
|
|
38
|
+
const index = subscribers.indexOf(subscriber)
|
|
39
|
+
if (index > -1) subscribers.splice(index, 1)
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
vi.mocked(getClient).mockReturnValue(currentClient)
|
|
46
|
+
vi.mocked(getSubscribableClient).mockImplementation(() => createSubscribable())
|
|
47
|
+
})
|
|
48
|
+
it('should return initial client', () => {
|
|
49
|
+
const {result} = renderHook(() => useClient({apiVersion: 'v2024-11-12'}))
|
|
50
|
+
|
|
51
|
+
expect(result.current.config().token).toBeUndefined()
|
|
52
|
+
expect(result.current.config().apiVersion).toBe('v2024-11-12')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should handle client update through authentication changes', async () => {
|
|
56
|
+
let clientSubscriber: Subscriber<SanityClient> | undefined
|
|
57
|
+
|
|
58
|
+
// Create a subscribable that can simulate updates
|
|
59
|
+
vi.mocked(getSubscribableClient).mockImplementation(() => ({
|
|
60
|
+
subscribe: (subscriber: Subscriber<SanityClient>) => {
|
|
61
|
+
clientSubscriber = subscriber
|
|
62
|
+
// Send initial client
|
|
63
|
+
subscriber.next(currentClient)
|
|
64
|
+
return {
|
|
65
|
+
unsubscribe: vi.fn(),
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}))
|
|
69
|
+
|
|
70
|
+
const {result} = renderHook(() => useClient({apiVersion: 'v2024-11-12'}))
|
|
71
|
+
|
|
72
|
+
// Verify initial state
|
|
73
|
+
expect(result.current.config().token).toBeUndefined()
|
|
74
|
+
expect(clientSubscriber).toBeDefined()
|
|
75
|
+
|
|
76
|
+
// Create authenticated client
|
|
77
|
+
const authenticatedClient = {
|
|
78
|
+
config: () => ({token: 'auth-token', apiVersion: 'v2024-11-12'}),
|
|
79
|
+
} as unknown as SanityClient
|
|
80
|
+
|
|
81
|
+
// Update getClient to return the new client
|
|
82
|
+
vi.mocked(getClient).mockReturnValue(authenticatedClient)
|
|
83
|
+
|
|
84
|
+
// Simulate the client update that would happen after auth change
|
|
85
|
+
await act(async () => {
|
|
86
|
+
clientSubscriber!.next(authenticatedClient)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Verify the client was updated with the new token
|
|
90
|
+
expect(result.current.config().token).toBe('auth-token')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should unsubscribe on unmount', () => {
|
|
94
|
+
const unsubscribeSpy = vi.fn()
|
|
95
|
+
vi.mocked(getSubscribableClient).mockImplementation(() => ({
|
|
96
|
+
subscribe: () => ({
|
|
97
|
+
unsubscribe: unsubscribeSpy,
|
|
98
|
+
}),
|
|
99
|
+
}))
|
|
100
|
+
|
|
101
|
+
const {unmount} = renderHook(() => useClient({apiVersion: 'v2024-11-12'}))
|
|
102
|
+
|
|
103
|
+
unmount()
|
|
104
|
+
expect(unsubscribeSpy).toHaveBeenCalled()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should handle subscription errors', () => {
|
|
108
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
109
|
+
|
|
110
|
+
const testError = new Error('Subscription error')
|
|
111
|
+
let errorSubscriber: Subscriber<SanityClient> | undefined
|
|
112
|
+
|
|
113
|
+
// Mock getSubscribableClient to create a subscription that will error
|
|
114
|
+
vi.mocked(getSubscribableClient).mockImplementation(() => ({
|
|
115
|
+
subscribe: (subscriber: Subscriber<SanityClient>) => {
|
|
116
|
+
errorSubscriber = subscriber
|
|
117
|
+
return {
|
|
118
|
+
unsubscribe: vi.fn(),
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
}))
|
|
122
|
+
|
|
123
|
+
renderHook(() => useClient({apiVersion: 'v2024-11-12'}))
|
|
124
|
+
|
|
125
|
+
errorSubscriber!.error(testError)
|
|
126
|
+
|
|
127
|
+
// eslint-disable-next-line no-console
|
|
128
|
+
expect(console.error).toHaveBeenCalledWith('Error in useClient subscription:', testError)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {type ClientOptions, getClient, getSubscribableClient} from '@sanity/sdk'
|
|
3
|
+
import {useCallback, useSyncExternalStore} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A React hook that provides a client that subscribes to changes in your application,
|
|
9
|
+
* such as user authentication changes.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* The hook uses `useSyncExternalStore` to safely subscribe to changes
|
|
13
|
+
* and ensure consistency between server and client rendering.
|
|
14
|
+
*
|
|
15
|
+
* @returns A Sanity client
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* function MyComponent() {
|
|
20
|
+
* const client = useClient()
|
|
21
|
+
* const [document, setDocument] = useState(null)
|
|
22
|
+
* useEffect(async () => {
|
|
23
|
+
* const doc = client.fetch('*[_id == "myDocumentId"]')
|
|
24
|
+
* setDocument(doc)
|
|
25
|
+
* }, [])
|
|
26
|
+
* return <div>{document ?? 'Loading...'}</div>
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export function useClient(options: ClientOptions): SanityClient {
|
|
33
|
+
const instance = useSanityInstance()
|
|
34
|
+
|
|
35
|
+
const subscribe = useCallback(
|
|
36
|
+
(onStoreChange: () => void) => {
|
|
37
|
+
const client$ = getSubscribableClient(options, instance)
|
|
38
|
+
const subscription = client$.subscribe({
|
|
39
|
+
next: onStoreChange,
|
|
40
|
+
error: (error) => {
|
|
41
|
+
// @TODO: We should tackle error handling / error boundaries soon
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.error('Error in useClient subscription:', error)
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
return () => subscription.unsubscribe()
|
|
47
|
+
},
|
|
48
|
+
[instance, options],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const getSnapshot = useCallback(() => {
|
|
52
|
+
return getClient(options, instance)
|
|
53
|
+
}, [instance, options])
|
|
54
|
+
|
|
55
|
+
return useSyncExternalStore(subscribe, getSnapshot)
|
|
56
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {renderHook} from '@testing-library/react'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {SanityProvider} from '../../components/context/SanityProvider'
|
|
7
|
+
import {useSanityInstance} from './useSanityInstance'
|
|
8
|
+
|
|
9
|
+
describe('useSanityInstance', () => {
|
|
10
|
+
const sanityInstance = createSanityInstance({projectId: 'test-project', dataset: 'production'})
|
|
11
|
+
|
|
12
|
+
it('returns sanity instance when used within provider', () => {
|
|
13
|
+
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
14
|
+
<SanityProvider sanityInstance={sanityInstance}>{children}</SanityProvider>
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const {result} = renderHook(() => useSanityInstance(), {wrapper})
|
|
18
|
+
|
|
19
|
+
expect(result.current).toBe(sanityInstance)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('throws error when used outside provider', () => {
|
|
23
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
24
|
+
|
|
25
|
+
expect(() => {
|
|
26
|
+
renderHook(() => useSanityInstance())
|
|
27
|
+
}).toThrow('useSanityInstance must be called from within the SanityProvider')
|
|
28
|
+
|
|
29
|
+
consoleSpy.mockRestore()
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {useContext} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SanityInstanceContext} from '../../components/context/SanityProvider'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook that provides the current Sanity instance from the context.
|
|
8
|
+
* This must be called from within a `SanityProvider` component.
|
|
9
|
+
* @public
|
|
10
|
+
* @returns the current Sanity instance
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const instance = useSanityInstance()
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const useSanityInstance = (): SanityInstance => {
|
|
17
|
+
const sanityInstance = useContext(SanityInstanceContext)
|
|
18
|
+
if (!sanityInstance) {
|
|
19
|
+
throw new Error('useSanityInstance must be called from within the SanityProvider')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return sanityInstance
|
|
23
|
+
}
|