@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.31
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 +6 -100
- package/dist/index.d.ts +2390 -2
- package/dist/index.js +1119 -2
- package/dist/index.js.map +1 -1
- package/package.json +35 -49
- package/src/_exports/index.ts +2 -10
- package/src/_exports/sdk-react.ts +73 -0
- package/src/components/SDKProvider.test.tsx +103 -0
- package/src/components/SDKProvider.tsx +52 -0
- package/src/components/SanityApp.test.tsx +244 -0
- package/src/components/SanityApp.tsx +106 -0
- package/src/components/auth/AuthBoundary.test.tsx +204 -29
- package/src/components/auth/AuthBoundary.tsx +96 -19
- package/src/components/auth/ConfigurationError.ts +22 -0
- package/src/components/auth/LoginCallback.test.tsx +22 -24
- package/src/components/auth/LoginCallback.tsx +6 -16
- package/src/components/auth/LoginError.test.tsx +11 -18
- package/src/components/auth/LoginError.tsx +43 -25
- package/src/components/utils.ts +22 -0
- package/src/context/ResourceProvider.test.tsx +157 -0
- package/src/context/ResourceProvider.tsx +111 -0
- package/src/context/SanityInstanceContext.ts +4 -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 +30 -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/useLoginUrl.tsx +14 -0
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
- package/src/hooks/client/useClient.ts +13 -33
- package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
- package/src/hooks/comlink/useFrameConnection.ts +107 -0
- package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
- package/src/hooks/comlink/useManageFavorite.ts +210 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
- package/src/hooks/comlink/useWindowConnection.ts +123 -0
- package/src/hooks/context/useSanityInstance.test.tsx +157 -15
- package/src/hooks/context/useSanityInstance.ts +68 -11
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
- package/src/hooks/datasets/useDatasets.test.ts +80 -0
- package/src/hooks/datasets/useDatasets.ts +52 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
- package/src/hooks/document/useApplyDocumentActions.ts +124 -0
- package/src/hooks/document/useDocument.test.ts +118 -0
- package/src/hooks/document/useDocument.ts +212 -0
- package/src/hooks/document/useDocumentEvent.test.ts +62 -0
- package/src/hooks/document/useDocumentEvent.ts +94 -0
- package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
- package/src/hooks/document/useDocumentPermissions.ts +131 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
- package/src/hooks/document/useEditDocument.test.ts +196 -0
- package/src/hooks/document/useEditDocument.ts +314 -0
- package/src/hooks/documents/useDocuments.test.tsx +179 -0
- package/src/hooks/documents/useDocuments.ts +300 -0
- package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
- package/src/hooks/helpers/createCallbackHook.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
- package/src/hooks/preview/usePreview.test.tsx +85 -17
- package/src/hooks/preview/usePreview.tsx +81 -22
- package/src/hooks/projection/useProjection.test.tsx +283 -0
- package/src/hooks/projection/useProjection.ts +232 -0
- package/src/hooks/projects/useProject.test.ts +80 -0
- package/src/hooks/projects/useProject.ts +51 -0
- package/src/hooks/projects/useProjects.test.ts +77 -0
- package/src/hooks/projects/useProjects.ts +45 -0
- package/src/hooks/query/useQuery.test.tsx +188 -0
- package/src/hooks/query/useQuery.ts +193 -0
- package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
- package/src/hooks/releases/useActiveReleases.ts +39 -0
- package/src/hooks/releases/usePerspective.test.tsx +120 -0
- package/src/hooks/releases/usePerspective.ts +49 -0
- package/src/hooks/users/useUsers.test.tsx +330 -0
- package/src/hooks/users/useUsers.ts +120 -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 -259
- package/dist/components.js +0 -301
- package/dist/components.js.map +0 -1
- package/dist/hooks.d.ts +0 -186
- 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/Login/LoginLinks.test.tsx +0 -100
- package/src/components/Login/LoginLinks.tsx +0 -73
- package/src/components/auth/Login.test.tsx +0 -41
- package/src/components/auth/Login.tsx +0 -45
- package/src/components/auth/LoginFooter.test.tsx +0 -29
- package/src/components/auth/LoginFooter.tsx +0 -65
- package/src/components/auth/LoginLayout.test.tsx +0 -33
- package/src/components/auth/LoginLayout.tsx +0 -81
- package/src/components/context/SanityProvider.test.tsx +0 -25
- 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/auth/useLoginUrls.test.tsx +0 -68
- package/src/hooks/auth/useLoginUrls.tsx +0 -51
- 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,85 @@
|
|
|
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
|
+
document: {
|
|
66
|
+
id: 'mock-id',
|
|
67
|
+
type: 'mock-type',
|
|
68
|
+
resource: {
|
|
69
|
+
id: 'mock-resource-id',
|
|
70
|
+
type: 'studio',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should handle errors when sending messages', () => {
|
|
77
|
+
vi.mocked(node.post).mockImplementation(() => {
|
|
78
|
+
throw new Error('Failed to send message')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const {result} = renderHook(() => useRecordDocumentHistoryEvent(mockDocumentHandle))
|
|
82
|
+
|
|
83
|
+
expect(() => result.current.recordEvent('viewed')).toThrow('Failed to send message')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
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 DocumentHandle, 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
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
interface UseRecordDocumentHistoryEventProps extends DocumentHandle {
|
|
24
|
+
resourceType: StudioResource['type'] | MediaResource['type'] | CanvasResource['type']
|
|
25
|
+
resourceId?: string
|
|
26
|
+
/**
|
|
27
|
+
* The name of the schema collection this document belongs to.
|
|
28
|
+
* Typically is the name of the workspace when used in the context of a studio.
|
|
29
|
+
*/
|
|
30
|
+
schemaName?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
* Hook for managing document interaction history in Sanity Studio.
|
|
36
|
+
* This hook provides functionality to record document interactions.
|
|
37
|
+
* @category History
|
|
38
|
+
* @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
|
|
39
|
+
* @returns An object containing:
|
|
40
|
+
* - `recordEvent` - Function to record document interactions
|
|
41
|
+
* - `isConnected` - Boolean indicating if connection to Studio is established
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* function MyDocumentAction(props: DocumentActionProps) {
|
|
46
|
+
* const {documentId, documentType, resourceType, resourceId} = props
|
|
47
|
+
* const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
|
|
48
|
+
* documentId,
|
|
49
|
+
* documentType,
|
|
50
|
+
* resourceType,
|
|
51
|
+
* resourceId,
|
|
52
|
+
* })
|
|
53
|
+
*
|
|
54
|
+
* return (
|
|
55
|
+
* <Button
|
|
56
|
+
* disabled={!isConnected}
|
|
57
|
+
* onClick={() => recordEvent('viewed')}
|
|
58
|
+
* text={'Viewed'}
|
|
59
|
+
* />
|
|
60
|
+
* )
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function useRecordDocumentHistoryEvent({
|
|
65
|
+
documentId,
|
|
66
|
+
documentType,
|
|
67
|
+
resourceType,
|
|
68
|
+
resourceId,
|
|
69
|
+
schemaName,
|
|
70
|
+
}: UseRecordDocumentHistoryEventProps): DocumentInteractionHistory {
|
|
71
|
+
const [status, setStatus] = useState<Status>('idle')
|
|
72
|
+
const {sendMessage} = useWindowConnection<Events.HistoryMessage, 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 recordEvent = useCallback(
|
|
83
|
+
(eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
|
|
84
|
+
try {
|
|
85
|
+
const message: Events.HistoryMessage = {
|
|
86
|
+
type: 'dashboard/v1/events/history',
|
|
87
|
+
data: {
|
|
88
|
+
eventType,
|
|
89
|
+
document: {
|
|
90
|
+
id: documentId,
|
|
91
|
+
type: documentType,
|
|
92
|
+
resource: {
|
|
93
|
+
id: resourceId!,
|
|
94
|
+
type: resourceType,
|
|
95
|
+
schemaName,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
sendMessage(message.type, message.data)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.error('Failed to record history event:', error)
|
|
105
|
+
throw error
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
[documentId, documentType, resourceId, resourceType, sendMessage, schemaName],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
recordEvent,
|
|
113
|
+
isConnected: status === 'connected',
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -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,123 @@
|
|
|
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
|
+
const node = getOrCreateNode(instance, {
|
|
67
|
+
name,
|
|
68
|
+
connectTo,
|
|
69
|
+
}) as unknown as Node<TWindowMessage, TFrameMessage>
|
|
70
|
+
nodeRef.current = node
|
|
71
|
+
|
|
72
|
+
const statusUnsubscribe = node.onStatus((eventStatus) => {
|
|
73
|
+
onStatus?.(eventStatus)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (onMessage) {
|
|
77
|
+
Object.entries(onMessage).forEach(([type, handler]) => {
|
|
78
|
+
const messageUnsubscribe = node.on(type, handler as WindowMessageHandler<TFrameMessage>)
|
|
79
|
+
messageUnsubscribers.current.push(messageUnsubscribe)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return () => {
|
|
84
|
+
statusUnsubscribe()
|
|
85
|
+
messageUnsubscribers.current.forEach((unsubscribe) => unsubscribe())
|
|
86
|
+
messageUnsubscribers.current = []
|
|
87
|
+
releaseNode(instance, name)
|
|
88
|
+
nodeRef.current = null
|
|
89
|
+
}
|
|
90
|
+
}, [instance, name, connectTo, onMessage, onStatus])
|
|
91
|
+
|
|
92
|
+
const sendMessage = useCallback(
|
|
93
|
+
(type: TWindowMessage['type'], data?: Extract<TWindowMessage, {type: typeof type}>['data']) => {
|
|
94
|
+
if (!nodeRef.current) {
|
|
95
|
+
throw new Error('Cannot send message before connection is established')
|
|
96
|
+
}
|
|
97
|
+
nodeRef.current.post(type, data)
|
|
98
|
+
},
|
|
99
|
+
[],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const fetch = useCallback(
|
|
103
|
+
<TResponse>(
|
|
104
|
+
type: string,
|
|
105
|
+
data?: MessageData,
|
|
106
|
+
fetchOptions?: {
|
|
107
|
+
responseTimeout?: number
|
|
108
|
+
signal?: AbortSignal
|
|
109
|
+
suppressWarnings?: boolean
|
|
110
|
+
},
|
|
111
|
+
): Promise<TResponse> => {
|
|
112
|
+
if (!nodeRef.current) {
|
|
113
|
+
throw new Error('Cannot fetch before connection is established')
|
|
114
|
+
}
|
|
115
|
+
return nodeRef.current?.fetch(type, data, fetchOptions ?? {}) as Promise<TResponse>
|
|
116
|
+
},
|
|
117
|
+
[],
|
|
118
|
+
)
|
|
119
|
+
return {
|
|
120
|
+
sendMessage,
|
|
121
|
+
fetch,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -1,31 +1,173 @@
|
|
|
1
|
-
import {createSanityInstance} from '@sanity/sdk'
|
|
1
|
+
import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {renderHook} from '@testing-library/react'
|
|
3
|
-
import
|
|
4
|
-
import {describe, expect, it
|
|
3
|
+
import {type ReactNode} from 'react'
|
|
4
|
+
import {describe, expect, it} from 'vitest'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
7
7
|
import {useSanityInstance} from './useSanityInstance'
|
|
8
8
|
|
|
9
9
|
describe('useSanityInstance', () => {
|
|
10
|
-
|
|
10
|
+
function createWrapper(instance: SanityInstance | null) {
|
|
11
|
+
return function Wrapper({children}: {children: ReactNode}) {
|
|
12
|
+
return (
|
|
13
|
+
<SanityInstanceContext.Provider value={instance}>{children}</SanityInstanceContext.Provider>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
11
17
|
|
|
12
|
-
it('
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
it('should return the Sanity instance from context', () => {
|
|
19
|
+
// Create a Sanity instance
|
|
20
|
+
const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
|
|
21
|
+
|
|
22
|
+
// Render the hook with the wrapper that provides the context
|
|
23
|
+
const {result} = renderHook(() => useSanityInstance(), {
|
|
24
|
+
wrapper: createWrapper(instance),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Check that the correct instance is returned
|
|
28
|
+
expect(result.current).toBe(instance)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should throw an error if no instance is found in context', () => {
|
|
32
|
+
// Expect the hook to throw when no instance is in context
|
|
33
|
+
expect(() => {
|
|
34
|
+
renderHook(() => useSanityInstance(), {
|
|
35
|
+
wrapper: createWrapper(null),
|
|
36
|
+
})
|
|
37
|
+
}).toThrow('SanityInstance context not found')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should include the requested config in error message when no instance found', () => {
|
|
41
|
+
const requestedConfig = {projectId: 'test', dataset: 'test'}
|
|
42
|
+
|
|
43
|
+
// Expect the hook to throw and include the requested config in the error
|
|
44
|
+
expect(() => {
|
|
45
|
+
renderHook(() => useSanityInstance(requestedConfig), {
|
|
46
|
+
wrapper: createWrapper(null),
|
|
47
|
+
})
|
|
48
|
+
}).toThrow(JSON.stringify(requestedConfig, null, 2))
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should find a matching instance with provided config', () => {
|
|
52
|
+
// Create a parent instance
|
|
53
|
+
const parentInstance = createSanityInstance({
|
|
54
|
+
projectId: 'parent-project',
|
|
55
|
+
dataset: 'parent-dataset',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Create a child instance
|
|
59
|
+
const childInstance = parentInstance.createChild({dataset: 'child-dataset'})
|
|
60
|
+
|
|
61
|
+
// Render the hook with the child instance and request the parent config
|
|
62
|
+
const {result} = renderHook(
|
|
63
|
+
() => useSanityInstance({projectId: 'parent-project', dataset: 'parent-dataset'}),
|
|
64
|
+
{wrapper: createWrapper(childInstance)},
|
|
15
65
|
)
|
|
16
66
|
|
|
17
|
-
|
|
67
|
+
// Should match and return the parent instance
|
|
68
|
+
expect(result.current).toBe(parentInstance)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should throw an error if no matching instance is found for config', () => {
|
|
72
|
+
// Create an instance
|
|
73
|
+
const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
|
|
18
74
|
|
|
19
|
-
|
|
75
|
+
// Request a config that doesn't match
|
|
76
|
+
const requestedConfig: SanityConfig = {
|
|
77
|
+
projectId: 'non-existent',
|
|
78
|
+
dataset: 'not-found',
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Expect the hook to throw for a non-matching config
|
|
82
|
+
expect(() => {
|
|
83
|
+
renderHook(() => useSanityInstance(requestedConfig), {
|
|
84
|
+
wrapper: createWrapper(instance),
|
|
85
|
+
})
|
|
86
|
+
}).toThrow('Could not find a matching Sanity instance')
|
|
20
87
|
})
|
|
21
88
|
|
|
22
|
-
it('
|
|
23
|
-
const
|
|
89
|
+
it('should include the requested config in error message when no matching instance', () => {
|
|
90
|
+
const instance = createSanityInstance({projectId: 'test-project', dataset: 'test-dataset'})
|
|
91
|
+
const requestedConfig = {projectId: 'different', dataset: 'different'}
|
|
24
92
|
|
|
93
|
+
// Expect the error to include the requested config details
|
|
25
94
|
expect(() => {
|
|
26
|
-
renderHook(() => useSanityInstance()
|
|
27
|
-
|
|
95
|
+
renderHook(() => useSanityInstance(requestedConfig), {
|
|
96
|
+
wrapper: createWrapper(instance),
|
|
97
|
+
})
|
|
98
|
+
}).toThrow(JSON.stringify(requestedConfig, null, 2))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should return the current instance when no config is provided', () => {
|
|
102
|
+
// Create a hierarchy of instances
|
|
103
|
+
const grandparent = createSanityInstance({projectId: 'gp', dataset: 'gp-ds'})
|
|
104
|
+
const parent = grandparent.createChild({projectId: 'p'})
|
|
105
|
+
const child = parent.createChild({dataset: 'child-ds'})
|
|
106
|
+
|
|
107
|
+
// Render the hook with the child instance and no config
|
|
108
|
+
const {result} = renderHook(() => useSanityInstance(), {
|
|
109
|
+
wrapper: createWrapper(child),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Should return the child instance
|
|
113
|
+
expect(result.current).toBe(child)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should match child instance when it satisfies the config', () => {
|
|
117
|
+
// Create a parent instance
|
|
118
|
+
const parent = createSanityInstance({projectId: 'parent', dataset: 'parent-ds'})
|
|
119
|
+
|
|
120
|
+
// Create a child instance that inherits projectId
|
|
121
|
+
const child = parent.createChild({dataset: 'child-ds'})
|
|
122
|
+
|
|
123
|
+
// Render the hook with the child instance and request by the child's dataset
|
|
124
|
+
const {result} = renderHook(() => useSanityInstance({dataset: 'child-ds'}), {
|
|
125
|
+
wrapper: createWrapper(child),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Should match and return the child instance
|
|
129
|
+
expect(result.current).toBe(child)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should match partial config correctly', () => {
|
|
133
|
+
// Create an instance with multiple config values
|
|
134
|
+
const instance = createSanityInstance({
|
|
135
|
+
projectId: 'test-proj',
|
|
136
|
+
dataset: 'test-ds',
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Should match when requesting just one property
|
|
140
|
+
const {result} = renderHook(() => useSanityInstance({dataset: 'test-ds'}), {
|
|
141
|
+
wrapper: createWrapper(instance),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(result.current).toBe(instance)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("should match deeper in hierarchy when current instance doesn't match", () => {
|
|
148
|
+
// Create a three-level hierarchy
|
|
149
|
+
const root = createSanityInstance({projectId: 'root', dataset: 'root-ds'})
|
|
150
|
+
const middle = root.createChild({projectId: 'middle'})
|
|
151
|
+
const leaf = middle.createChild({dataset: 'leaf-ds'})
|
|
152
|
+
|
|
153
|
+
// Request config matching the root from the leaf
|
|
154
|
+
const {result} = renderHook(() => useSanityInstance({projectId: 'root', dataset: 'root-ds'}), {
|
|
155
|
+
wrapper: createWrapper(leaf),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Should find and return the root instance
|
|
159
|
+
expect(result.current).toBe(root)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should match undefined values in config', () => {
|
|
163
|
+
// Create instance with only projectId
|
|
164
|
+
const rootInstance = createSanityInstance({projectId: 'test'})
|
|
165
|
+
|
|
166
|
+
// Match specifically looking for undefined dataset
|
|
167
|
+
const {result} = renderHook(() => useSanityInstance({dataset: undefined}), {
|
|
168
|
+
wrapper: createWrapper(rootInstance),
|
|
169
|
+
})
|
|
28
170
|
|
|
29
|
-
|
|
171
|
+
expect(result.current).toBe(rootInstance)
|
|
30
172
|
})
|
|
31
173
|
})
|