@sanity/sdk-react 0.0.0-alpha.2 → 0.0.0-alpha.21
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 +38 -67
- package/dist/index.d.ts +4811 -2
- package/dist/index.js +1069 -2
- package/dist/index.js.map +1 -1
- package/package.json +27 -58
- package/src/_exports/index.ts +66 -10
- package/src/components/Login/LoginLinks.test.tsx +4 -14
- package/src/components/Login/LoginLinks.tsx +16 -31
- package/src/components/SDKProvider.test.tsx +79 -0
- package/src/components/SDKProvider.tsx +42 -0
- package/src/components/SanityApp.test.tsx +156 -0
- package/src/components/SanityApp.tsx +90 -0
- package/src/components/auth/AuthBoundary.test.tsx +6 -19
- package/src/components/auth/AuthBoundary.tsx +20 -4
- package/src/components/auth/Login.test.tsx +2 -16
- package/src/components/auth/Login.tsx +11 -30
- package/src/components/auth/LoginCallback.test.tsx +5 -20
- package/src/components/auth/LoginCallback.tsx +9 -14
- package/src/components/auth/LoginError.test.tsx +2 -17
- package/src/components/auth/LoginError.tsx +11 -16
- package/src/components/auth/LoginFooter.test.tsx +2 -16
- package/src/components/auth/LoginFooter.tsx +8 -24
- package/src/components/auth/LoginLayout.test.tsx +2 -16
- package/src/components/auth/LoginLayout.tsx +8 -38
- package/src/components/auth/authTestHelpers.tsx +11 -0
- package/src/components/utils.ts +22 -0
- package/src/context/SanityInstanceContext.ts +4 -0
- package/src/{components/context → context}/SanityProvider.test.tsx +2 -2
- package/src/context/SanityProvider.tsx +50 -0
- package/src/hooks/_synchronous-groq-js.mjs +4 -0
- package/src/hooks/auth/useAuthState.tsx +4 -5
- package/src/hooks/auth/useAuthToken.tsx +1 -1
- package/src/hooks/auth/useCurrentUser.tsx +28 -4
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
- package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
- package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
- package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
- package/src/hooks/auth/useLogOut.test.tsx +2 -2
- package/src/hooks/auth/useLogOut.tsx +1 -1
- package/src/hooks/auth/useLoginUrls.tsx +1 -0
- package/src/hooks/client/useClient.ts +9 -30
- package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
- package/src/hooks/comlink/useFrameConnection.ts +107 -0
- package/src/hooks/comlink/useManageFavorite.test.ts +111 -0
- package/src/hooks/comlink/useManageFavorite.ts +130 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +81 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +106 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
- package/src/hooks/comlink/useWindowConnection.ts +122 -0
- package/src/hooks/context/useSanityInstance.test.tsx +2 -2
- package/src/hooks/context/useSanityInstance.ts +24 -8
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +178 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +123 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +278 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +92 -0
- package/src/hooks/datasets/useDatasets.ts +40 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +25 -0
- package/src/hooks/document/useApplyDocumentActions.ts +75 -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/useDocumentPermissions.ts +84 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +33 -0
- package/src/hooks/document/useEditDocument.test.ts +179 -0
- package/src/hooks/document/useEditDocument.ts +195 -0
- package/src/hooks/documents/useDocuments.test.tsx +152 -0
- package/src/hooks/documents/useDocuments.ts +174 -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/paginatedDocuments/usePaginatedDocuments.test.tsx +259 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +290 -0
- package/src/hooks/preview/usePreview.test.tsx +19 -10
- package/src/hooks/preview/usePreview.tsx +67 -13
- package/src/hooks/projection/useProjection.test.tsx +218 -0
- package/src/hooks/projection/useProjection.ts +147 -0
- package/src/hooks/projects/useProject.ts +48 -0
- package/src/hooks/projects/useProjects.ts +45 -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/src/vite-env.d.ts +10 -0
- package/dist/_chunks-es/useLogOut.js +0 -44
- package/dist/_chunks-es/useLogOut.js.map +0 -1
- package/dist/assets/bundle-CcAyERuZ.css +0 -11
- package/dist/components.d.ts +0 -257
- package/dist/components.js +0 -316
- package/dist/components.js.map +0 -1
- package/dist/hooks.d.ts +0 -187
- package/dist/hooks.js +0 -81
- package/dist/hooks.js.map +0 -1
- package/src/_exports/components.ts +0 -13
- package/src/_exports/hooks.ts +0 -9
- package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
- package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
- package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
- package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
- package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
- package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
- package/src/components/context/SanityProvider.tsx +0 -42
- package/src/css/css.config.js +0 -220
- package/src/css/paramour.css +0 -2347
- package/src/css/styles.css +0 -11
- package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
- 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 -87
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {type Status} from '@sanity/comlink'
|
|
2
|
+
import {
|
|
3
|
+
type CanvasResource,
|
|
4
|
+
type Events,
|
|
5
|
+
type MediaResource,
|
|
6
|
+
SDK_CHANNEL_NAME,
|
|
7
|
+
SDK_NODE_NAME,
|
|
8
|
+
type StudioResource,
|
|
9
|
+
} from '@sanity/message-protocol'
|
|
10
|
+
import {type FrameMessage} from '@sanity/sdk'
|
|
11
|
+
import {useCallback, useState} from 'react'
|
|
12
|
+
|
|
13
|
+
import {useWindowConnection} from './useWindowConnection'
|
|
14
|
+
|
|
15
|
+
// should we import this whole type from the message protocol?
|
|
16
|
+
|
|
17
|
+
interface ManageFavorite {
|
|
18
|
+
favorite: () => void
|
|
19
|
+
unfavorite: () => void
|
|
20
|
+
isFavorited: boolean
|
|
21
|
+
isConnected: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface UseManageFavoriteProps {
|
|
25
|
+
documentId: string
|
|
26
|
+
documentType: string
|
|
27
|
+
resourceId?: string
|
|
28
|
+
resourceType: StudioResource['type'] | MediaResource['type'] | CanvasResource['type']
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @beta
|
|
33
|
+
*
|
|
34
|
+
* ## useManageFavorite
|
|
35
|
+
* This hook provides functionality to add and remove documents from favorites,
|
|
36
|
+
* and tracks the current favorite status of the document.
|
|
37
|
+
* @category Dashboard Communication
|
|
38
|
+
* @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
|
|
39
|
+
* @returns An object containing:
|
|
40
|
+
* - `favorite` - Function to add document to favorites
|
|
41
|
+
* - `unfavorite` - Function to remove document from favorites
|
|
42
|
+
* - `isFavorited` - Boolean indicating if document is currently favorited
|
|
43
|
+
* - `isConnected` - Boolean indicating if connection to Dashboard UI is established
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* function MyDocumentAction(props: DocumentActionProps) {
|
|
48
|
+
* const {documentId, documentType} = props
|
|
49
|
+
* const {favorite, unfavorite, isFavorited, isConnected} = useManageFavorite({
|
|
50
|
+
* documentId,
|
|
51
|
+
* documentType
|
|
52
|
+
* })
|
|
53
|
+
*
|
|
54
|
+
* return (
|
|
55
|
+
* <Button
|
|
56
|
+
* disabled={!isConnected}
|
|
57
|
+
* onClick={() => isFavorited ? unfavorite() : favorite()}
|
|
58
|
+
* text={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
|
|
59
|
+
* />
|
|
60
|
+
* )
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function useManageFavorite({
|
|
65
|
+
documentId,
|
|
66
|
+
documentType,
|
|
67
|
+
resourceId,
|
|
68
|
+
resourceType,
|
|
69
|
+
}: UseManageFavoriteProps): ManageFavorite {
|
|
70
|
+
const [isFavorited, setIsFavorited] = useState(false) // should load this from a comlink fetch
|
|
71
|
+
const [status, setStatus] = useState<Status>('idle')
|
|
72
|
+
const {sendMessage} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
|
|
73
|
+
name: SDK_NODE_NAME,
|
|
74
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
75
|
+
onStatus: setStatus,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (resourceType !== 'studio' && !resourceId) {
|
|
79
|
+
throw new Error('resourceId is required for media-library and canvas resources')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handleFavoriteAction = useCallback(
|
|
83
|
+
(action: 'added' | 'removed', setFavoriteState: boolean) => {
|
|
84
|
+
if (!documentId || !documentType || !resourceType) return
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const message: Events.FavoriteMessage = {
|
|
88
|
+
type: 'dashboard/v1/events/favorite/mutate',
|
|
89
|
+
data: {
|
|
90
|
+
eventType: action,
|
|
91
|
+
documentId,
|
|
92
|
+
documentType,
|
|
93
|
+
resourceType,
|
|
94
|
+
// Resource Id should exist for media-library and canvas resources
|
|
95
|
+
resourceId: resourceId!,
|
|
96
|
+
},
|
|
97
|
+
response: {
|
|
98
|
+
success: true,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
sendMessage(message.type, message.data)
|
|
103
|
+
setIsFavorited(setFavoriteState)
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const error = err instanceof Error ? err : new Error('Failed to update favorite status')
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.error(
|
|
108
|
+
`Failed to ${action === 'added' ? 'favorite' : 'unfavorite'} document:`,
|
|
109
|
+
error,
|
|
110
|
+
)
|
|
111
|
+
throw error
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
[documentId, documentType, resourceId, resourceType, sendMessage],
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const favorite = useCallback(() => handleFavoriteAction('added', true), [handleFavoriteAction])
|
|
118
|
+
|
|
119
|
+
const unfavorite = useCallback(
|
|
120
|
+
() => handleFavoriteAction('removed', false),
|
|
121
|
+
[handleFavoriteAction],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
favorite,
|
|
126
|
+
unfavorite,
|
|
127
|
+
isFavorited,
|
|
128
|
+
isConnected: status === 'connected',
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
documentId: 'mock-id',
|
|
23
|
+
documentType: 'mock-type',
|
|
24
|
+
resourceType: 'studio' as const,
|
|
25
|
+
resourceId: 'mock-resource-id',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createMockNode() {
|
|
29
|
+
return {
|
|
30
|
+
on: vi.fn(() => () => {}),
|
|
31
|
+
post: vi.fn(),
|
|
32
|
+
stop: vi.fn(),
|
|
33
|
+
onStatus: vi.fn((callback) => {
|
|
34
|
+
statusCallback = callback
|
|
35
|
+
return () => {}
|
|
36
|
+
}),
|
|
37
|
+
} as unknown as Node<Message, Message>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
statusCallback = null
|
|
42
|
+
node = createMockNode()
|
|
43
|
+
vi.mocked(getOrCreateNode).mockReturnValue(node)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should initialize with correct connection status', () => {
|
|
47
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
48
|
+
|
|
49
|
+
expect(result.current.isConnected).toBe(false)
|
|
50
|
+
|
|
51
|
+
act(() => {
|
|
52
|
+
statusCallback?.('connected')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(result.current.isConnected).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should send correct message when recording events', () => {
|
|
59
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
60
|
+
|
|
61
|
+
result.current.recordEvent('viewed')
|
|
62
|
+
|
|
63
|
+
expect(node.post).toHaveBeenCalledWith('dashboard/v1/events/history', {
|
|
64
|
+
eventType: 'viewed',
|
|
65
|
+
documentId: 'mock-id',
|
|
66
|
+
documentType: 'mock-type',
|
|
67
|
+
resourceType: 'studio',
|
|
68
|
+
resourceId: 'mock-resource-id',
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should handle errors when sending messages', () => {
|
|
73
|
+
vi.mocked(node.post).mockImplementation(() => {
|
|
74
|
+
throw new Error('Failed to send message')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
78
|
+
|
|
79
|
+
expect(() => result.current.recordEvent('viewed')).toThrow('Failed to send message')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {type Status} from '@sanity/comlink'
|
|
2
|
+
import {
|
|
3
|
+
type CanvasResource,
|
|
4
|
+
type Events,
|
|
5
|
+
type MediaResource,
|
|
6
|
+
SDK_CHANNEL_NAME,
|
|
7
|
+
SDK_NODE_NAME,
|
|
8
|
+
type StudioResource,
|
|
9
|
+
} from '@sanity/message-protocol'
|
|
10
|
+
import {type FrameMessage} from '@sanity/sdk'
|
|
11
|
+
import {useCallback, useState} from 'react'
|
|
12
|
+
|
|
13
|
+
import {useWindowConnection} from './useWindowConnection'
|
|
14
|
+
|
|
15
|
+
interface DocumentInteractionHistory {
|
|
16
|
+
recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
|
|
17
|
+
isConnected: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
interface UseRecordDocumentHistoryEventProps {
|
|
24
|
+
documentId: string
|
|
25
|
+
documentType: string
|
|
26
|
+
resourceType: StudioResource['type'] | MediaResource['type'] | CanvasResource['type']
|
|
27
|
+
resourceId?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @public
|
|
32
|
+
* Hook for managing document interaction history in Sanity Studio.
|
|
33
|
+
* This hook provides functionality to record document interactions.
|
|
34
|
+
* @category History
|
|
35
|
+
* @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
|
|
36
|
+
* @returns An object containing:
|
|
37
|
+
* - `recordEvent` - Function to record document interactions
|
|
38
|
+
* - `isConnected` - Boolean indicating if connection to Studio is established
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* function MyDocumentAction(props: DocumentActionProps) {
|
|
43
|
+
* const {documentId, documentType, resourceType, resourceId} = props
|
|
44
|
+
* const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
|
|
45
|
+
* documentId,
|
|
46
|
+
* documentType,
|
|
47
|
+
* resourceType,
|
|
48
|
+
* resourceId,
|
|
49
|
+
* })
|
|
50
|
+
*
|
|
51
|
+
* return (
|
|
52
|
+
* <Button
|
|
53
|
+
* disabled={!isConnected}
|
|
54
|
+
* onClick={() => recordEvent('viewed')}
|
|
55
|
+
* text={'Viewed'}
|
|
56
|
+
* />
|
|
57
|
+
* )
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function useRecordDocumentHistoryEvent({
|
|
62
|
+
documentId,
|
|
63
|
+
documentType,
|
|
64
|
+
resourceType,
|
|
65
|
+
resourceId,
|
|
66
|
+
}: UseRecordDocumentHistoryEventProps): DocumentInteractionHistory {
|
|
67
|
+
const [status, setStatus] = useState<Status>('idle')
|
|
68
|
+
const {sendMessage} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
|
|
69
|
+
name: SDK_NODE_NAME,
|
|
70
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
71
|
+
onStatus: setStatus,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
if (resourceType !== 'studio' && !resourceId) {
|
|
75
|
+
throw new Error('resourceId is required for media-library and canvas resources')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const recordEvent = useCallback(
|
|
79
|
+
(eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
|
|
80
|
+
try {
|
|
81
|
+
const message: Events.HistoryMessage = {
|
|
82
|
+
type: 'dashboard/v1/events/history',
|
|
83
|
+
data: {
|
|
84
|
+
eventType,
|
|
85
|
+
documentId,
|
|
86
|
+
documentType,
|
|
87
|
+
resourceType,
|
|
88
|
+
resourceId: resourceId!,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
sendMessage(message.type, message.data)
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.error('Failed to record history event:', error)
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
[documentId, documentType, resourceId, resourceType, sendMessage],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
recordEvent,
|
|
104
|
+
isConnected: status === 'connected',
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {type Message, type Node, type Status} from '@sanity/comlink'
|
|
2
|
+
import {getOrCreateNode, releaseNode} from '@sanity/sdk'
|
|
3
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {act, renderHook} from '../../../test/test-utils'
|
|
6
|
+
import {useWindowConnection} from './useWindowConnection'
|
|
7
|
+
|
|
8
|
+
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal()
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
getOrCreateNode: vi.fn(),
|
|
13
|
+
createNode: vi.fn(),
|
|
14
|
+
releaseNode: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
interface TestMessage {
|
|
18
|
+
type: 'TEST_MESSAGE'
|
|
19
|
+
data: {someData: string}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AnotherMessage {
|
|
23
|
+
type: 'ANOTHER_MESSAGE'
|
|
24
|
+
data: {otherData: number}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type TestMessages = TestMessage | AnotherMessage
|
|
28
|
+
|
|
29
|
+
describe('useWindowConnection', () => {
|
|
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
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
statusCallback = null
|
|
48
|
+
node = createMockNode()
|
|
49
|
+
vi.mocked(getOrCreateNode).mockReturnValue(node as unknown as Node<Message, Message>)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should call onStatus callback when status changes', () => {
|
|
53
|
+
const onStatusMock = vi.fn()
|
|
54
|
+
renderHook(() =>
|
|
55
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
56
|
+
name: 'test',
|
|
57
|
+
connectTo: 'window',
|
|
58
|
+
onStatus: onStatusMock,
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
act(() => {
|
|
63
|
+
statusCallback?.('connected')
|
|
64
|
+
})
|
|
65
|
+
expect(onStatusMock).toHaveBeenCalledWith('connected')
|
|
66
|
+
|
|
67
|
+
act(() => {
|
|
68
|
+
statusCallback?.('disconnected')
|
|
69
|
+
})
|
|
70
|
+
expect(onStatusMock).toHaveBeenCalledWith('disconnected')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should not throw if onStatus is not provided', () => {
|
|
74
|
+
renderHook(() =>
|
|
75
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
76
|
+
name: 'test',
|
|
77
|
+
connectTo: 'window',
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(() => {
|
|
82
|
+
act(() => {
|
|
83
|
+
statusCallback?.('connected')
|
|
84
|
+
})
|
|
85
|
+
}).not.toThrow()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should register message handlers', () => {
|
|
89
|
+
const mockHandler = vi.fn()
|
|
90
|
+
const mockData = {someData: 'test'}
|
|
91
|
+
|
|
92
|
+
renderHook(() =>
|
|
93
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
94
|
+
name: 'test',
|
|
95
|
+
connectTo: 'window',
|
|
96
|
+
onMessage: {
|
|
97
|
+
TEST_MESSAGE: mockHandler,
|
|
98
|
+
ANOTHER_MESSAGE: vi.fn(),
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const onCallback = vi.mocked(node.on).mock.calls[0][1]
|
|
104
|
+
onCallback(mockData)
|
|
105
|
+
|
|
106
|
+
expect(mockHandler).toHaveBeenCalledWith(mockData)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should send messages through the node', () => {
|
|
110
|
+
const {result} = renderHook(() =>
|
|
111
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
112
|
+
name: 'test',
|
|
113
|
+
connectTo: 'window',
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
result.current.sendMessage('TEST_MESSAGE', {someData: 'test'})
|
|
118
|
+
expect(node.post).toHaveBeenCalledWith('TEST_MESSAGE', {someData: 'test'})
|
|
119
|
+
|
|
120
|
+
result.current.sendMessage('ANOTHER_MESSAGE', {otherData: 123})
|
|
121
|
+
expect(node.post).toHaveBeenCalledWith('ANOTHER_MESSAGE', {otherData: 123})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should cleanup on unmount', () => {
|
|
125
|
+
const {unmount} = renderHook(() =>
|
|
126
|
+
useWindowConnection<TestMessages, TestMessages>({
|
|
127
|
+
name: 'test',
|
|
128
|
+
connectTo: 'window',
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
unmount()
|
|
133
|
+
expect(releaseNode).toHaveBeenCalled()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {type MessageData, type Node, type Status} from '@sanity/comlink'
|
|
2
|
+
import {type FrameMessage, getOrCreateNode, releaseNode, type WindowMessage} from '@sanity/sdk'
|
|
3
|
+
import {useCallback, useEffect, useRef} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
export type WindowMessageHandler<TFrameMessage extends FrameMessage> = (
|
|
11
|
+
event: TFrameMessage['data'],
|
|
12
|
+
) => TFrameMessage['response']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export interface UseWindowConnectionOptions<TMessage extends FrameMessage> {
|
|
18
|
+
name: string
|
|
19
|
+
connectTo: string
|
|
20
|
+
onMessage?: Record<TMessage['type'], WindowMessageHandler<TMessage>>
|
|
21
|
+
onStatus?: (status: Status) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export interface WindowConnection<TMessage extends WindowMessage> {
|
|
28
|
+
sendMessage: <TType extends TMessage['type']>(
|
|
29
|
+
type: TType,
|
|
30
|
+
data?: Extract<TMessage, {type: TType}>['data'],
|
|
31
|
+
) => void
|
|
32
|
+
fetch: <TResponse>(
|
|
33
|
+
type: string,
|
|
34
|
+
data?: MessageData,
|
|
35
|
+
options?: {
|
|
36
|
+
signal?: AbortSignal
|
|
37
|
+
suppressWarnings?: boolean
|
|
38
|
+
responseTimeout?: number
|
|
39
|
+
},
|
|
40
|
+
) => Promise<TResponse>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @internal
|
|
45
|
+
* Hook to wrap a Comlink node in a React hook.
|
|
46
|
+
* Our store functionality takes care of the lifecycle of the node,
|
|
47
|
+
* as well as sharing a single node between invocations if they share the same name.
|
|
48
|
+
*
|
|
49
|
+
* Generally not to be used directly, but to be used as a dependency of
|
|
50
|
+
* Comlink-powered hooks like `useManageFavorite`.
|
|
51
|
+
*/
|
|
52
|
+
export function useWindowConnection<
|
|
53
|
+
TWindowMessage extends WindowMessage,
|
|
54
|
+
TFrameMessage extends FrameMessage,
|
|
55
|
+
>({
|
|
56
|
+
name,
|
|
57
|
+
connectTo,
|
|
58
|
+
onMessage,
|
|
59
|
+
onStatus,
|
|
60
|
+
}: UseWindowConnectionOptions<TFrameMessage>): WindowConnection<TWindowMessage> {
|
|
61
|
+
const nodeRef = useRef<Node<TWindowMessage, TFrameMessage> | null>(null)
|
|
62
|
+
const messageUnsubscribers = useRef<(() => void)[]>([])
|
|
63
|
+
const instance = useSanityInstance()
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
// the type cast is unfortunate, but the generic type of the node is not known here.
|
|
67
|
+
// We know that the node is a WindowMessage node, but not the generic types.
|
|
68
|
+
const node = getOrCreateNode(instance, {
|
|
69
|
+
name,
|
|
70
|
+
connectTo,
|
|
71
|
+
}) as unknown as Node<TWindowMessage, TFrameMessage>
|
|
72
|
+
nodeRef.current = node
|
|
73
|
+
|
|
74
|
+
const statusUnsubscribe = node.onStatus((eventStatus) => {
|
|
75
|
+
onStatus?.(eventStatus)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (onMessage) {
|
|
79
|
+
Object.entries(onMessage).forEach(([type, handler]) => {
|
|
80
|
+
const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)
|
|
81
|
+
messageUnsubscribers.current.push(messageUnsubscribe)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
statusUnsubscribe()
|
|
87
|
+
messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())
|
|
88
|
+
messageUnsubscribers.current = []
|
|
89
|
+
releaseNode(instance, name)
|
|
90
|
+
nodeRef.current = null
|
|
91
|
+
}
|
|
92
|
+
}, [instance, name, connectTo, onMessage, onStatus])
|
|
93
|
+
|
|
94
|
+
const sendMessage = useCallback(
|
|
95
|
+
(type: TWindowMessage['type'], data?: Extract<TWindowMessage, {type: typeof type}>['data']) => {
|
|
96
|
+
if (!nodeRef.current) {
|
|
97
|
+
throw new Error('Cannot send message before connection is established')
|
|
98
|
+
}
|
|
99
|
+
nodeRef.current.post(type, data)
|
|
100
|
+
},
|
|
101
|
+
[],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const fetch = useCallback(
|
|
105
|
+
<TResponse>(
|
|
106
|
+
type: string,
|
|
107
|
+
data?: MessageData,
|
|
108
|
+
fetchOptions?: {
|
|
109
|
+
responseTimeout?: number
|
|
110
|
+
signal?: AbortSignal
|
|
111
|
+
suppressWarnings?: boolean
|
|
112
|
+
},
|
|
113
|
+
): Promise<TResponse> => {
|
|
114
|
+
return nodeRef.current?.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>
|
|
115
|
+
},
|
|
116
|
+
[],
|
|
117
|
+
)
|
|
118
|
+
return {
|
|
119
|
+
sendMessage,
|
|
120
|
+
fetch,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -3,7 +3,7 @@ import {renderHook} from '@testing-library/react'
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import {describe, expect, it, vi} from 'vitest'
|
|
5
5
|
|
|
6
|
-
import {SanityProvider} from '../../
|
|
6
|
+
import {SanityProvider} from '../../context/SanityProvider'
|
|
7
7
|
import {useSanityInstance} from './useSanityInstance'
|
|
8
8
|
|
|
9
9
|
describe('useSanityInstance', () => {
|
|
@@ -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
|
-
import type
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {useContext} from 'react'
|
|
3
3
|
|
|
4
|
-
import {SanityInstanceContext} from '../../
|
|
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
|
-
* @
|
|
10
|
-
*
|
|
9
|
+
* @internal
|
|
10
|
+
*
|
|
11
|
+
* @param resourceId - The resourceId of the Sanity instance to return (optional)
|
|
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
|
}
|