@sanity/sdk-react 0.0.0-alpha.8 → 0.0.0-rc.0
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 +33 -126
- package/dist/index.d.ts +4641 -2
- package/dist/index.js +960 -2
- package/dist/index.js.map +1 -1
- package/package.json +17 -40
- package/src/_exports/index.ts +58 -10
- package/src/components/Login/LoginLinks.test.tsx +90 -0
- package/src/components/Login/LoginLinks.tsx +58 -0
- package/src/components/SDKProvider.test.tsx +79 -0
- package/src/components/SDKProvider.tsx +42 -0
- package/src/components/SanityApp.test.tsx +104 -2
- package/src/components/SanityApp.tsx +54 -17
- package/src/components/auth/AuthBoundary.test.tsx +2 -2
- package/src/components/auth/AuthBoundary.tsx +13 -3
- package/src/components/auth/Login.test.tsx +1 -1
- package/src/components/auth/Login.tsx +11 -26
- package/src/components/auth/LoginCallback.tsx +4 -7
- package/src/components/auth/LoginError.tsx +12 -8
- package/src/components/auth/LoginFooter.tsx +13 -20
- package/src/components/auth/LoginLayout.tsx +8 -9
- package/src/components/auth/authTestHelpers.tsx +1 -8
- package/src/components/utils.ts +22 -0
- package/src/context/SanityInstanceContext.ts +4 -0
- package/src/context/SanityProvider.test.tsx +1 -1
- package/src/context/SanityProvider.tsx +10 -8
- package/src/hooks/_synchronous-groq-js.mjs +4 -0
- package/src/hooks/auth/useAuthState.tsx +0 -2
- package/src/hooks/auth/useCurrentUser.tsx +26 -20
- package/src/hooks/client/useClient.ts +8 -30
- package/src/hooks/comlink/useFrameConnection.test.tsx +45 -10
- package/src/hooks/comlink/useFrameConnection.ts +24 -5
- package/src/hooks/comlink/useManageFavorite.test.ts +106 -0
- package/src/hooks/comlink/useManageFavorite.ts +98 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +77 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +75 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +43 -12
- package/src/hooks/comlink/useWindowConnection.ts +13 -1
- package/src/hooks/context/useSanityInstance.test.tsx +1 -1
- package/src/hooks/context/useSanityInstance.ts +21 -5
- package/src/hooks/datasets/useDatasets.ts +37 -0
- package/src/hooks/document/useApplyActions.test.ts +25 -0
- package/src/hooks/document/useApplyActions.ts +74 -0
- package/src/hooks/document/useDocument.test.ts +81 -0
- package/src/hooks/document/useDocument.ts +107 -0
- package/src/hooks/document/useDocumentEvent.test.ts +63 -0
- package/src/hooks/document/useDocumentEvent.ts +54 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +30 -0
- package/src/hooks/document/useEditDocument.test.ts +179 -0
- package/src/hooks/document/useEditDocument.ts +195 -0
- package/src/hooks/document/usePermissions.ts +82 -0
- package/src/hooks/helpers/createCallbackHook.tsx +3 -2
- package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
- package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
- package/src/hooks/infiniteList/useInfiniteList.test.tsx +152 -0
- package/src/hooks/infiniteList/useInfiniteList.ts +174 -0
- package/src/hooks/paginatedList/usePaginatedList.test.tsx +259 -0
- package/src/hooks/paginatedList/usePaginatedList.ts +290 -0
- package/src/hooks/preview/usePreview.tsx +7 -4
- package/src/hooks/projection/useProjection.test.tsx +218 -0
- package/src/hooks/projection/useProjection.ts +135 -0
- package/src/hooks/projects/useProject.ts +45 -0
- package/src/hooks/projects/useProjects.ts +41 -0
- package/src/hooks/query/useQuery.test.tsx +188 -0
- package/src/hooks/query/useQuery.ts +103 -0
- package/src/hooks/users/useUsers.test.ts +163 -0
- package/src/hooks/users/useUsers.ts +107 -0
- package/src/utils/getEnv.ts +21 -0
- package/src/version.ts +8 -0
- package/dist/_chunks-es/context.js +0 -8
- package/dist/_chunks-es/context.js.map +0 -1
- package/dist/_chunks-es/useLogOut.js +0 -44
- package/dist/_chunks-es/useLogOut.js.map +0 -1
- package/dist/components.d.ts +0 -111
- package/dist/components.js +0 -153
- package/dist/components.js.map +0 -1
- package/dist/context.d.ts +0 -45
- package/dist/context.js +0 -5
- package/dist/context.js.map +0 -1
- package/dist/hooks.d.ts +0 -3485
- package/dist/hooks.js +0 -167
- package/dist/hooks.js.map +0 -1
- package/src/_exports/components.ts +0 -2
- package/src/_exports/context.ts +0 -2
- package/src/_exports/hooks.ts +0 -27
- package/src/hooks/client/useClient.test.tsx +0 -130
- package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
- package/src/hooks/documentCollection/useDocuments.ts +0 -135
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
2
|
+
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
3
|
+
import {useCallback, useState} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useWindowConnection} from './useWindowConnection'
|
|
6
|
+
|
|
7
|
+
// should we import this whole type from the message protocol?
|
|
8
|
+
|
|
9
|
+
interface ManageFavorite {
|
|
10
|
+
favorite: () => void
|
|
11
|
+
unfavorite: () => void
|
|
12
|
+
isFavorited: boolean
|
|
13
|
+
isConnected: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @beta
|
|
18
|
+
*
|
|
19
|
+
* ## useManageFavorite
|
|
20
|
+
* This hook provides functionality to add and remove documents from favorites,
|
|
21
|
+
* and tracks the current favorite status of the document.
|
|
22
|
+
* @category Core UI Communication
|
|
23
|
+
* @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
|
|
24
|
+
* @returns An object containing:
|
|
25
|
+
* - `favorite` - Function to add document to favorites
|
|
26
|
+
* - `unfavorite` - Function to remove document from favorites
|
|
27
|
+
* - `isFavorited` - Boolean indicating if document is currently favorited
|
|
28
|
+
* - `isConnected` - Boolean indicating if connection to Core UI is established
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* function MyDocumentAction(props: DocumentActionProps) {
|
|
33
|
+
* const {_id, _type} = props
|
|
34
|
+
* const {favorite, unfavorite, isFavorited, isConnected} = useManageFavorite({
|
|
35
|
+
* _id,
|
|
36
|
+
* _type
|
|
37
|
+
* })
|
|
38
|
+
*
|
|
39
|
+
* return (
|
|
40
|
+
* <Button
|
|
41
|
+
* disabled={!isConnected}
|
|
42
|
+
* onClick={() => isFavorited ? unfavorite() : favorite()}
|
|
43
|
+
* text={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
|
|
44
|
+
* />
|
|
45
|
+
* )
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function useManageFavorite({_id, _type}: DocumentHandle): ManageFavorite {
|
|
50
|
+
const [isFavorited, setIsFavorited] = useState(false) // should load this from a comlink fetch
|
|
51
|
+
const {sendMessage, status} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
|
|
52
|
+
name: SDK_NODE_NAME,
|
|
53
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const handleFavoriteAction = useCallback(
|
|
57
|
+
(action: 'added' | 'removed', setFavoriteState: boolean) => {
|
|
58
|
+
if (!_id || !_type) return
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const message: Events.FavoriteMessage = {
|
|
62
|
+
type: 'core/v1/events/favorite',
|
|
63
|
+
data: {
|
|
64
|
+
eventType: action,
|
|
65
|
+
documentId: _id,
|
|
66
|
+
documentType: _type,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sendMessage(message.type, message.data)
|
|
71
|
+
setIsFavorited(setFavoriteState)
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const error = err instanceof Error ? err : new Error('Failed to update favorite status')
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.error(
|
|
76
|
+
`Failed to ${action === 'added' ? 'favorite' : 'unfavorite'} document:`,
|
|
77
|
+
error,
|
|
78
|
+
)
|
|
79
|
+
throw error
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
[_id, _type, sendMessage],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
const favorite = useCallback(() => handleFavoriteAction('added', true), [handleFavoriteAction])
|
|
86
|
+
|
|
87
|
+
const unfavorite = useCallback(
|
|
88
|
+
() => handleFavoriteAction('removed', false),
|
|
89
|
+
[handleFavoriteAction],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
favorite,
|
|
94
|
+
unfavorite,
|
|
95
|
+
isFavorited,
|
|
96
|
+
isConnected: status === 'connected',
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {type Message, type Node, type Status} from '@sanity/comlink'
|
|
2
|
+
import {getOrCreateNode} from '@sanity/sdk'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {act, renderHook} from '../../../test/test-utils'
|
|
6
|
+
import {useRecordDocumentHistoryEvent} from './useRecordDocumentHistoryEvent'
|
|
7
|
+
|
|
8
|
+
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal()
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
getOrCreateNode: vi.fn(),
|
|
13
|
+
releaseNode: vi.fn(),
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('useRecordDocumentHistoryEvent', () => {
|
|
18
|
+
let node: Node<Message, Message>
|
|
19
|
+
let statusCallback: ((status: Status) => void) | null = null
|
|
20
|
+
|
|
21
|
+
const mockDocumentHandle = {
|
|
22
|
+
_id: 'mock-id',
|
|
23
|
+
_type: 'mock-type',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createMockNode() {
|
|
27
|
+
return {
|
|
28
|
+
on: vi.fn(() => () => {}),
|
|
29
|
+
post: vi.fn(),
|
|
30
|
+
stop: vi.fn(),
|
|
31
|
+
onStatus: vi.fn((callback) => {
|
|
32
|
+
statusCallback = callback
|
|
33
|
+
return () => {}
|
|
34
|
+
}),
|
|
35
|
+
} as unknown as Node<Message, Message>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
statusCallback = null
|
|
40
|
+
node = createMockNode()
|
|
41
|
+
vi.mocked(getOrCreateNode).mockReturnValue(node)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should initialize with correct connection status', () => {
|
|
45
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
46
|
+
|
|
47
|
+
expect(result.current.isConnected).toBe(false)
|
|
48
|
+
|
|
49
|
+
act(() => {
|
|
50
|
+
statusCallback?.('connected')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(result.current.isConnected).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should send correct message when recording events', () => {
|
|
57
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
58
|
+
|
|
59
|
+
result.current.recordEvent('viewed')
|
|
60
|
+
|
|
61
|
+
expect(node.post).toHaveBeenCalledWith('core/v1/events/history', {
|
|
62
|
+
eventType: 'viewed',
|
|
63
|
+
documentId: 'mock-id',
|
|
64
|
+
documentType: 'mock-type',
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should handle errors when sending messages', () => {
|
|
69
|
+
vi.mocked(node.post).mockImplementation(() => {
|
|
70
|
+
throw new Error('Failed to send message')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
74
|
+
|
|
75
|
+
expect(() => result.current.recordEvent('viewed')).toThrow('Failed to send message')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
2
|
+
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
3
|
+
import {useCallback} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useWindowConnection} from './useWindowConnection'
|
|
6
|
+
|
|
7
|
+
interface DocumentInteractionHistory {
|
|
8
|
+
recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
|
|
9
|
+
isConnected: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @public
|
|
14
|
+
* Hook for managing document interaction history in Sanity Studio.
|
|
15
|
+
* This hook provides functionality to record document interactions.
|
|
16
|
+
* @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
|
|
17
|
+
* @returns An object containing:
|
|
18
|
+
* - `recordEvent` - Function to record document interactions
|
|
19
|
+
* - `isConnected` - Boolean indicating if connection to Studio is established
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* function MyDocumentAction(props: DocumentActionProps) {
|
|
24
|
+
* const {_id, _type} = props
|
|
25
|
+
* const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
|
|
26
|
+
* _id,
|
|
27
|
+
* _type
|
|
28
|
+
* })
|
|
29
|
+
*
|
|
30
|
+
* return (
|
|
31
|
+
* <Button
|
|
32
|
+
* disabled={!isConnected}
|
|
33
|
+
* onClick={() => recordEvent('viewed')}
|
|
34
|
+
* text={'Viewed'}
|
|
35
|
+
* />
|
|
36
|
+
* )
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useRecordDocumentHistoryEvent({
|
|
41
|
+
_id,
|
|
42
|
+
_type,
|
|
43
|
+
}: DocumentHandle): DocumentInteractionHistory {
|
|
44
|
+
const {sendMessage, status} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
|
|
45
|
+
name: SDK_NODE_NAME,
|
|
46
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const recordEvent = useCallback(
|
|
50
|
+
(eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
|
|
51
|
+
try {
|
|
52
|
+
const message: Events.HistoryMessage = {
|
|
53
|
+
type: 'core/v1/events/history',
|
|
54
|
+
data: {
|
|
55
|
+
eventType,
|
|
56
|
+
documentId: _id,
|
|
57
|
+
documentType: _type,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
sendMessage(message.type, message.data)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
// eslint-disable-next-line no-console
|
|
64
|
+
console.error('Failed to record history event:', error)
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
[_id, _type, sendMessage],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
recordEvent,
|
|
73
|
+
isConnected: status === 'connected',
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {type Message, type Node} from '@sanity/comlink'
|
|
1
|
+
import {type Message, type Node, type Status} from '@sanity/comlink'
|
|
2
2
|
import {getOrCreateNode, releaseNode} from '@sanity/sdk'
|
|
3
3
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
4
|
|
|
5
|
-
import {renderHook} from '../../../test/test-utils'
|
|
5
|
+
import {act, renderHook} from '../../../test/test-utils'
|
|
6
6
|
import {useWindowConnection} from './useWindowConnection'
|
|
7
7
|
|
|
8
8
|
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
@@ -26,24 +26,55 @@ interface AnotherMessage {
|
|
|
26
26
|
|
|
27
27
|
type TestMessages = TestMessage | AnotherMessage
|
|
28
28
|
|
|
29
|
-
function createMockNode() {
|
|
30
|
-
return {
|
|
31
|
-
// return unsubscribe function
|
|
32
|
-
on: vi.fn(() => () => {}),
|
|
33
|
-
post: vi.fn(),
|
|
34
|
-
stop: vi.fn(),
|
|
35
|
-
} as unknown as Node<Message, Message>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
29
|
describe('useWindowConnection', () => {
|
|
39
30
|
let node: Node<Message, Message>
|
|
31
|
+
let statusCallback: ((status: Status) => void) | null = null
|
|
32
|
+
|
|
33
|
+
function createMockNode() {
|
|
34
|
+
return {
|
|
35
|
+
// return unsubscribe function
|
|
36
|
+
on: vi.fn(() => () => {}),
|
|
37
|
+
post: vi.fn(),
|
|
38
|
+
stop: vi.fn(),
|
|
39
|
+
onStatus: vi.fn((callback) => {
|
|
40
|
+
statusCallback = callback
|
|
41
|
+
return () => {}
|
|
42
|
+
}),
|
|
43
|
+
} as unknown as Node<Message, Message>
|
|
44
|
+
}
|
|
40
45
|
|
|
41
46
|
beforeEach(() => {
|
|
47
|
+
statusCallback = null
|
|
42
48
|
node = createMockNode()
|
|
43
|
-
|
|
44
49
|
vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
|
|
45
50
|
})
|
|
46
51
|
|
|
52
|
+
it('should initialize with idle status', () => {
|
|
53
|
+
const {result} = renderHook(() =>
|
|
54
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
55
|
+
name: 'test',
|
|
56
|
+
connectTo: 'window',
|
|
57
|
+
}),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
expect(result.current.status).toBe('idle')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should update status to connected when node connects', async () => {
|
|
64
|
+
const {result} = renderHook(() =>
|
|
65
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
66
|
+
name: 'test',
|
|
67
|
+
connectTo: 'window',
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
expect(result.current.status).toBe('idle')
|
|
72
|
+
act(() => {
|
|
73
|
+
statusCallback?.('connected')
|
|
74
|
+
})
|
|
75
|
+
expect(result.current.status).toBe('connected')
|
|
76
|
+
})
|
|
77
|
+
|
|
47
78
|
it('should register message handlers', () => {
|
|
48
79
|
const mockHandler = vi.fn()
|
|
49
80
|
const mockData = {someData: 'test'}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import {type Status} from '@sanity/comlink'
|
|
1
2
|
import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
|
|
2
|
-
import {useCallback, useEffect, useMemo} from 'react'
|
|
3
|
+
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
3
4
|
|
|
4
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
6
|
|
|
@@ -27,6 +28,7 @@ export interface WindowConnection<TMessage extends WindowMessage> {
|
|
|
27
28
|
type: TType,
|
|
28
29
|
data?: Extract<TMessage, {type: TType}>['data'],
|
|
29
30
|
) => void
|
|
31
|
+
status: Status
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -38,12 +40,21 @@ export function useWindowConnection<
|
|
|
38
40
|
>(options: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {
|
|
39
41
|
const {name, onMessage, connectTo} = options
|
|
40
42
|
const instance = useSanityInstance()
|
|
43
|
+
const [status, setStatus] = useState<Status>('idle')
|
|
41
44
|
|
|
42
45
|
const node = useMemo(
|
|
43
46
|
() => getOrCreateNode(instance, {name, connectTo}),
|
|
44
47
|
[instance, name, connectTo],
|
|
45
48
|
)
|
|
46
49
|
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const unsubscribe = node.onStatus((newStatus) => {
|
|
52
|
+
setStatus(newStatus)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return unsubscribe
|
|
56
|
+
}, [node, instance, name])
|
|
57
|
+
|
|
47
58
|
useEffect(() => {
|
|
48
59
|
if (!onMessage) return
|
|
49
60
|
|
|
@@ -78,5 +89,6 @@ export function useWindowConnection<
|
|
|
78
89
|
|
|
79
90
|
return {
|
|
80
91
|
sendMessage,
|
|
92
|
+
status,
|
|
81
93
|
}
|
|
82
94
|
}
|
|
@@ -11,7 +11,7 @@ describe('useSanityInstance', () => {
|
|
|
11
11
|
|
|
12
12
|
it('returns sanity instance when used within provider', () => {
|
|
13
13
|
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
14
|
-
<SanityProvider
|
|
14
|
+
<SanityProvider sanityInstances={[sanityInstance]}>{children}</SanityProvider>
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
const {result} = renderHook(() => useSanityInstance(), {wrapper})
|
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
import {type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {useContext} from 'react'
|
|
3
3
|
|
|
4
|
-
import {SanityInstanceContext} from '../../context/
|
|
4
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* `useSanityInstance` returns the current Sanity instance from the application context.
|
|
8
8
|
* This must be called from within a `SanityProvider` component.
|
|
9
|
-
* @
|
|
9
|
+
* @internal
|
|
10
|
+
*
|
|
11
|
+
* @param resourceId - The resourceId of the Sanity instance to return (optional)
|
|
10
12
|
* @returns The current Sanity instance
|
|
11
13
|
* @example
|
|
12
14
|
* ```tsx
|
|
13
|
-
* const instance = useSanityInstance()
|
|
15
|
+
* const instance = useSanityInstance('abc123.production')
|
|
14
16
|
* ```
|
|
15
17
|
*/
|
|
16
|
-
export const useSanityInstance = (): SanityInstance => {
|
|
18
|
+
export const useSanityInstance = (resourceId?: string): SanityInstance => {
|
|
17
19
|
const sanityInstance = useContext(SanityInstanceContext)
|
|
18
20
|
if (!sanityInstance) {
|
|
19
21
|
throw new Error('useSanityInstance must be called from within the SanityProvider')
|
|
20
22
|
}
|
|
23
|
+
if (sanityInstance.length === 0) {
|
|
24
|
+
throw new Error('No Sanity instances found')
|
|
25
|
+
}
|
|
26
|
+
if (sanityInstance.length === 1 || !resourceId) {
|
|
27
|
+
return sanityInstance[0]
|
|
28
|
+
}
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
if (!resourceId) {
|
|
31
|
+
throw new Error('resourceId is required when there are multiple Sanity instances')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const instance = sanityInstance.find((inst) => inst.identity.resourceId === resourceId)
|
|
35
|
+
if (!instance) {
|
|
36
|
+
throw new Error(`Sanity instance with resourceId ${resourceId} not found`)
|
|
37
|
+
}
|
|
38
|
+
return instance
|
|
23
39
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {type DatasetsResponse} from '@sanity/client'
|
|
2
|
+
import {getDatasetsState, resolveDatasets, type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
3
|
+
|
|
4
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
|
+
|
|
6
|
+
type UseDatasets = {
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
* Returns metadata for each dataset in your organization.
|
|
10
|
+
*
|
|
11
|
+
* @category Datasets
|
|
12
|
+
* @returns The metadata for your organization's datasets
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const datasets = useDatasets()
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <select>
|
|
20
|
+
* {datasets.map((dataset) => (
|
|
21
|
+
* <option key={dataset.name}>{dataset.name}</option>
|
|
22
|
+
* ))}
|
|
23
|
+
* </select>
|
|
24
|
+
* )
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
*/
|
|
28
|
+
(): DatasetsResponse
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @public */
|
|
32
|
+
export const useDatasets: UseDatasets = createStateSourceHook({
|
|
33
|
+
// remove `undefined` since we're suspending when that is the case
|
|
34
|
+
getState: getDatasetsState as (instance: SanityInstance) => StateSource<DatasetsResponse>,
|
|
35
|
+
shouldSuspend: (instance) => getDatasetsState(instance).getCurrent() === undefined,
|
|
36
|
+
suspender: resolveDatasets,
|
|
37
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {applyActions, createDocument, type ResourceId} from '@sanity/sdk'
|
|
2
|
+
import {describe, it} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
5
|
+
|
|
6
|
+
vi.mock('../helpers/createCallbackHook', () => ({
|
|
7
|
+
createCallbackHook: vi.fn((cb) => () => cb),
|
|
8
|
+
}))
|
|
9
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
10
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
11
|
+
return {...original, applyActions: vi.fn()}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('useApplyActions', () => {
|
|
15
|
+
it('calls `createCallbackHook` with `applyActions`', async () => {
|
|
16
|
+
const {useApplyActions} = await import('./useApplyActions')
|
|
17
|
+
const resourceId: ResourceId = 'project1.dataset1'
|
|
18
|
+
expect(createCallbackHook).not.toHaveBeenCalled()
|
|
19
|
+
|
|
20
|
+
expect(applyActions).not.toHaveBeenCalled()
|
|
21
|
+
const apply = useApplyActions(resourceId)
|
|
22
|
+
apply(createDocument({_type: 'author'}))
|
|
23
|
+
expect(applyActions).toHaveBeenCalledWith(createDocument({_type: 'author'}))
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ActionsResult,
|
|
3
|
+
applyActions,
|
|
4
|
+
type ApplyActionsOptions,
|
|
5
|
+
type DocumentAction,
|
|
6
|
+
type ResourceId,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {type SanityDocument} from '@sanity/types'
|
|
9
|
+
|
|
10
|
+
import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* @beta
|
|
15
|
+
*
|
|
16
|
+
* Provides a callback for applying one or more actions to a document.
|
|
17
|
+
*
|
|
18
|
+
* @category Documents
|
|
19
|
+
* @param resourceId - The resource ID of the document to apply actions to. If not provided, the document will use the default resource.
|
|
20
|
+
* @returns A function that takes one more more {@link DocumentAction}s and returns a promise that resolves to an {@link ActionsResult}.
|
|
21
|
+
* @example Publish or unpublish a document
|
|
22
|
+
* ```
|
|
23
|
+
* import { publishDocument, unpublishDocument } from '@sanity/sdk'
|
|
24
|
+
* import { useApplyActions } from '@sanity/sdk-react'
|
|
25
|
+
*
|
|
26
|
+
* const apply = useApplyActions()
|
|
27
|
+
* const myDocument = { _id: 'my-document-id', _type: 'my-document-type' }
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <button onClick={() => apply(publishDocument(myDocument))}>Publish</button>
|
|
31
|
+
* <button onClick={() => apply(unpublishDocument(myDocument))}>Unpublish</button>
|
|
32
|
+
* )
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @example Create and publish a new document
|
|
36
|
+
* ```
|
|
37
|
+
* import { createDocument, publishDocument } from '@sanity/sdk'
|
|
38
|
+
* import { useApplyActions } from '@sanity/sdk-react'
|
|
39
|
+
*
|
|
40
|
+
* const apply = useApplyActions()
|
|
41
|
+
*
|
|
42
|
+
* const handleCreateAndPublish = () => {
|
|
43
|
+
* const handle = { _id: window.crypto.randomUUID(), _type: 'my-document-type' }
|
|
44
|
+
* apply([
|
|
45
|
+
* createDocument(handle),
|
|
46
|
+
* publishDocument(handle),
|
|
47
|
+
* ])
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* return (
|
|
51
|
+
* <button onClick={handleCreateAndPublish}>
|
|
52
|
+
* I’m feeling lucky
|
|
53
|
+
* </button>
|
|
54
|
+
* )
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function useApplyActions(
|
|
58
|
+
resourceId?: ResourceId,
|
|
59
|
+
): <TDocument extends SanityDocument>(
|
|
60
|
+
action: DocumentAction<TDocument> | DocumentAction<TDocument>[],
|
|
61
|
+
options?: ApplyActionsOptions,
|
|
62
|
+
) => Promise<ActionsResult<TDocument>>
|
|
63
|
+
|
|
64
|
+
/** @beta */
|
|
65
|
+
export function useApplyActions(
|
|
66
|
+
resourceId?: ResourceId,
|
|
67
|
+
): (
|
|
68
|
+
action: DocumentAction | DocumentAction[],
|
|
69
|
+
options?: ApplyActionsOptions,
|
|
70
|
+
) => Promise<ActionsResult> {
|
|
71
|
+
return _useApplyActions(resourceId)()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const _useApplyActions = (resourceId?: ResourceId) => createCallbackHook(applyActions, resourceId)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// tests/useDocument.test.ts
|
|
2
|
+
import {
|
|
3
|
+
createSanityInstance,
|
|
4
|
+
getDocumentState,
|
|
5
|
+
resolveDocument,
|
|
6
|
+
type StateSource,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {type SanityDocument} from '@sanity/types'
|
|
9
|
+
import {renderHook} from '@testing-library/react'
|
|
10
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
11
|
+
|
|
12
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
13
|
+
import {useDocument} from './useDocument'
|
|
14
|
+
|
|
15
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
16
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
17
|
+
return {...original, getDocumentState: vi.fn(), resolveDocument: vi.fn()}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
21
|
+
useSanityInstance: vi.fn(),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
// Create a fake instance to be returned by useSanityInstance.
|
|
25
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
26
|
+
const doc: SanityDocument = {
|
|
27
|
+
_id: 'doc1',
|
|
28
|
+
foo: 'bar',
|
|
29
|
+
_type: 'book',
|
|
30
|
+
_rev: 'tx0',
|
|
31
|
+
_createdAt: '2025-02-06T00:11:00.000Z',
|
|
32
|
+
_updatedAt: '2025-02-06T00:11:00.000Z',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('useDocument hook', () => {
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.resetAllMocks()
|
|
38
|
+
vi.mocked(useSanityInstance).mockReturnValue(instance)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns the current document when ready (without a path)', () => {
|
|
42
|
+
const getCurrent = vi.fn().mockReturnValue(doc)
|
|
43
|
+
const subscribe = vi.fn().mockReturnValue(vi.fn())
|
|
44
|
+
vi.mocked(getDocumentState).mockReturnValue({
|
|
45
|
+
getCurrent,
|
|
46
|
+
subscribe,
|
|
47
|
+
} as unknown as StateSource<unknown>)
|
|
48
|
+
|
|
49
|
+
const {result} = renderHook(() => useDocument({_id: 'doc1', _type: 'book'}))
|
|
50
|
+
|
|
51
|
+
expect(result.current).toEqual(doc)
|
|
52
|
+
expect(getCurrent).toHaveBeenCalled()
|
|
53
|
+
expect(subscribe).toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('throws a promise (suspends) when the document is not ready', () => {
|
|
57
|
+
const getCurrent = vi.fn().mockReturnValue(undefined)
|
|
58
|
+
const subscribe = vi.fn().mockReturnValue(vi.fn())
|
|
59
|
+
vi.mocked(getDocumentState).mockReturnValue({
|
|
60
|
+
getCurrent,
|
|
61
|
+
subscribe,
|
|
62
|
+
} as unknown as StateSource<unknown>)
|
|
63
|
+
|
|
64
|
+
const resolveDocPromise = Promise.resolve(doc)
|
|
65
|
+
|
|
66
|
+
// Also, simulate resolveDocument to return a known promise.
|
|
67
|
+
vi.mocked(resolveDocument).mockReturnValue(resolveDocPromise)
|
|
68
|
+
|
|
69
|
+
// Render the hook and capture the thrown promise.
|
|
70
|
+
const {result} = renderHook(() => {
|
|
71
|
+
try {
|
|
72
|
+
return useDocument({_id: 'doc1', _type: 'book'})
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return e
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// When the document is not ready, the hook throws the promise from resolveDocument.
|
|
79
|
+
expect(result.current).toBe(resolveDocPromise)
|
|
80
|
+
})
|
|
81
|
+
})
|