@sanity/sdk-react 0.0.0-alpha.12 → 0.0.0-alpha.14

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.
@@ -0,0 +1,163 @@
1
+ import {createUsersStore, type ResourceType, type SanityUser} from '@sanity/sdk'
2
+ import {act, renderHook} from '@testing-library/react'
3
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
4
+
5
+ import {useSanityInstance} from '../context/useSanityInstance'
6
+ import {useUsers} from './useUsers'
7
+
8
+ vi.mock('@sanity/sdk')
9
+ vi.mock('../context/useSanityInstance')
10
+
11
+ describe('useUsers', () => {
12
+ const mockInstance = {}
13
+ const mockUser: SanityUser = {
14
+ profile: {
15
+ id: 'user1',
16
+ displayName: 'Test User',
17
+ email: 'test@test.com',
18
+ provider: 'test',
19
+ createdAt: '2021-01-01',
20
+ },
21
+ sanityUserId: 'user1',
22
+ memberships: [],
23
+ }
24
+
25
+ const getCurrent = vi.fn().mockReturnValue({
26
+ users: [],
27
+ totalCount: 0,
28
+ nextCursor: null,
29
+ hasMore: false,
30
+ initialFetchCompleted: false,
31
+ options: {
32
+ resourceType: '' as ResourceType,
33
+ resourceId: '',
34
+ limit: 100,
35
+ },
36
+ })
37
+ const unsubscribe = vi.fn()
38
+ const subscribe = vi.fn().mockReturnValue(unsubscribe)
39
+ const dispose = vi.fn()
40
+
41
+ const mockUsersStore: ReturnType<typeof createUsersStore> = {
42
+ setOptions: vi.fn(),
43
+ loadMore: vi.fn(),
44
+ resolveUsers: vi.fn(),
45
+ getState: vi.fn().mockReturnValue({getCurrent, subscribe}),
46
+ dispose,
47
+ }
48
+
49
+ beforeEach(() => {
50
+ vi.mocked(useSanityInstance).mockReturnValue(
51
+ mockInstance as unknown as ReturnType<typeof useSanityInstance>,
52
+ )
53
+ vi.mocked(createUsersStore).mockReturnValue(
54
+ mockUsersStore as unknown as ReturnType<typeof createUsersStore>,
55
+ )
56
+ })
57
+
58
+ afterEach(() => {
59
+ vi.clearAllMocks()
60
+ })
61
+
62
+ it('should initialize with given params', () => {
63
+ renderHook(() =>
64
+ useUsers({
65
+ resourceType: 'project',
66
+ resourceId: 'proj1',
67
+ }),
68
+ )
69
+
70
+ expect(createUsersStore).toHaveBeenCalledWith(mockInstance)
71
+ expect(mockUsersStore.setOptions).toHaveBeenCalledWith({
72
+ resourceType: 'project',
73
+ resourceId: 'proj1',
74
+ })
75
+ })
76
+
77
+ it('should subscribe to users store changes', () => {
78
+ renderHook(() =>
79
+ useUsers({
80
+ resourceType: 'organization',
81
+ resourceId: 'org1',
82
+ }),
83
+ )
84
+ expect(subscribe).toHaveBeenCalledTimes(1)
85
+ })
86
+
87
+ it('should return current users state', () => {
88
+ const mockState = {users: [mockUser], hasMore: true}
89
+ getCurrent.mockReturnValue(mockState)
90
+
91
+ const {result} = renderHook(() =>
92
+ useUsers({
93
+ resourceType: 'project',
94
+ resourceId: 'proj1',
95
+ }),
96
+ )
97
+ expect(result.current).toMatchObject(mockState)
98
+ })
99
+
100
+ it('should call loadMore when loadMore is invoked', () => {
101
+ const {result} = renderHook(() =>
102
+ useUsers({
103
+ resourceType: 'project',
104
+ resourceId: 'proj1',
105
+ }),
106
+ )
107
+
108
+ act(() => {
109
+ result.current.loadMore()
110
+ })
111
+
112
+ expect(mockUsersStore.loadMore).toHaveBeenCalled()
113
+ })
114
+
115
+ it('should update options when params change', () => {
116
+ const initialParams = {resourceType: 'project' as ResourceType, resourceId: 'proj1'}
117
+ const {rerender} = renderHook(({params}) => useUsers(params), {
118
+ initialProps: {params: initialParams},
119
+ })
120
+
121
+ const newParams = {resourceType: 'organization' as ResourceType, resourceId: 'org1'}
122
+ rerender({params: newParams})
123
+
124
+ expect(mockUsersStore.setOptions).toHaveBeenCalledWith(newParams)
125
+ })
126
+
127
+ it('should resolve users if initial fetch not completed', () => {
128
+ getCurrent.mockReturnValue({initialFetchCompleted: false})
129
+
130
+ renderHook(() =>
131
+ useUsers({
132
+ resourceType: 'project',
133
+ resourceId: 'proj1',
134
+ }),
135
+ )
136
+ expect(mockUsersStore.resolveUsers).toHaveBeenCalled()
137
+ })
138
+
139
+ it('should not resolve users if initial fetch already completed', () => {
140
+ getCurrent.mockReturnValue({initialFetchCompleted: true})
141
+
142
+ renderHook(() =>
143
+ useUsers({
144
+ resourceType: 'project',
145
+ resourceId: 'proj1',
146
+ }),
147
+ )
148
+ expect(mockUsersStore.resolveUsers).not.toHaveBeenCalled()
149
+ })
150
+
151
+ it('should clean up store on unmount', () => {
152
+ const {unmount} = renderHook(() =>
153
+ useUsers({
154
+ resourceType: 'project',
155
+ resourceId: 'proj1',
156
+ }),
157
+ )
158
+
159
+ unmount()
160
+ expect(mockUsersStore.dispose).toHaveBeenCalled()
161
+ expect(unsubscribe).toHaveBeenCalled()
162
+ })
163
+ })
@@ -0,0 +1,107 @@
1
+ import {createUsersStore, type ResourceType, type SanityUser} from '@sanity/sdk'
2
+ import {useCallback, useEffect, useState, useSyncExternalStore} from 'react'
3
+
4
+ import {useSanityInstance} from '../context/useSanityInstance'
5
+
6
+ /**
7
+ * @public
8
+ * @category Users
9
+ */
10
+ export interface UseUsersParams {
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 Users
28
+ */
29
+ export interface UseUsersResult {
30
+ /**
31
+ * The users fetched.
32
+ */
33
+ users: SanityUser[]
34
+ /**
35
+ * Whether there are more users to fetch.
36
+ */
37
+ hasMore: boolean
38
+ /**
39
+ * Load more users.
40
+ */
41
+ loadMore: () => void
42
+ }
43
+
44
+ /**
45
+ *
46
+ * @public
47
+ *
48
+ * Retrieves the users for a given resource (either a project or an organization).
49
+ *
50
+ * @category Users
51
+ * @param params - The resource type and its ID, and the limit of users to fetch
52
+ * @returns A list of users, a boolean indicating whether there are more users to fetch, and a function to load more users
53
+ *
54
+ * @example
55
+ * ```
56
+ * const { users, hasMore, loadMore } = useUsers({
57
+ * resourceType: 'organization',
58
+ * resourceId: 'my-org-id',
59
+ * limit: 10,
60
+ * })
61
+ *
62
+ * return (
63
+ * <div>
64
+ * {users.map(user => (
65
+ * <figure key={user.sanityUserId}>
66
+ * <img src={user.profile.imageUrl} alt='' />
67
+ * <figcaption>{user.profile.displayName}</figcaption>
68
+ * <address>{user.profile.email}</address>
69
+ * </figure>
70
+ * ))}
71
+ * {hasMore && <button onClick={loadMore}>Load More</button>}
72
+ * </div>
73
+ * )
74
+ * ```
75
+ */
76
+ export function useUsers(params: UseUsersParams): UseUsersResult {
77
+ const instance = useSanityInstance()
78
+ const [store] = useState(() => createUsersStore(instance))
79
+
80
+ useEffect(() => {
81
+ store.setOptions({
82
+ resourceType: params.resourceType,
83
+ resourceId: params.resourceId,
84
+ })
85
+ }, [params.resourceType, params.resourceId, store])
86
+
87
+ const subscribe = useCallback(
88
+ (onStoreChanged: () => void) => {
89
+ if (store.getState().getCurrent().initialFetchCompleted === false) {
90
+ store.resolveUsers()
91
+ }
92
+ const unsubscribe = store.getState().subscribe(onStoreChanged)
93
+
94
+ return () => {
95
+ unsubscribe()
96
+ store.dispose()
97
+ }
98
+ },
99
+ [store],
100
+ )
101
+
102
+ const getSnapshot = useCallback(() => store.getState().getCurrent(), [store])
103
+
104
+ const {users, hasMore} = useSyncExternalStore(subscribe, getSnapshot) || {}
105
+
106
+ return {users, hasMore, loadMore: store.loadMore}
107
+ }
@@ -1,130 +0,0 @@
1
- import {type SanityClient} from '@sanity/client'
2
- import {act} from '@testing-library/react'
3
- import {type Subscribable, type 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
- })