@sanity/sdk-react 0.0.0-alpha.9 → 0.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -126
- package/dist/index.d.ts +4742 -2
- package/dist/index.js +1054 -2
- package/dist/index.js.map +1 -1
- package/package.json +19 -43
- package/src/_exports/index.ts +66 -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/auth/useDashboardOrganizationId.test.tsx +42 -0
- package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
- package/src/hooks/client/useClient.ts +8 -30
- package/src/hooks/comlink/useFrameConnection.test.tsx +55 -10
- package/src/hooks/comlink/useFrameConnection.ts +39 -43
- package/src/hooks/comlink/useManageFavorite.test.ts +106 -0
- package/src/hooks/comlink/useManageFavorite.ts +101 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +77 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +79 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +53 -12
- package/src/hooks/comlink/useWindowConnection.ts +69 -29
- package/src/hooks/context/useSanityInstance.test.tsx +1 -1
- package/src/hooks/context/useSanityInstance.ts +21 -5
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +97 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +274 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +91 -0
- package/src/hooks/datasets/useDatasets.ts +37 -0
- package/src/hooks/document/useApplyActions.test.ts +5 -4
- package/src/hooks/document/useApplyActions.ts +55 -5
- package/src/hooks/document/useDocument.test.ts +2 -2
- package/src/hooks/document/useDocument.ts +90 -21
- package/src/hooks/document/useDocumentEvent.test.ts +13 -3
- package/src/hooks/document/useDocumentEvent.ts +36 -4
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -1
- package/src/hooks/document/useDocumentSyncStatus.ts +26 -2
- package/src/hooks/document/useEditDocument.test.ts +55 -10
- package/src/hooks/document/useEditDocument.ts +159 -31
- 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.test.tsx +6 -6
- package/src/hooks/preview/usePreview.tsx +12 -9
- package/src/hooks/projection/useProjection.test.tsx +218 -0
- package/src/hooks/projection/useProjection.ts +147 -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 -45
- 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 -3532
- package/dist/hooks.js +0 -218
- 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 -32
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {type ChannelInstance, type Controller, type Status} from '@sanity/comlink'
|
|
1
2
|
import {
|
|
2
3
|
type FrameMessage,
|
|
3
4
|
getOrCreateChannel,
|
|
@@ -5,7 +6,7 @@ import {
|
|
|
5
6
|
releaseChannel,
|
|
6
7
|
type WindowMessage,
|
|
7
8
|
} from '@sanity/sdk'
|
|
8
|
-
import {useCallback, useEffect,
|
|
9
|
+
import {useCallback, useEffect, useRef} from 'react'
|
|
9
10
|
|
|
10
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
11
12
|
|
|
@@ -23,7 +24,11 @@ export interface UseFrameConnectionOptions<TWindowMessage extends WindowMessage>
|
|
|
23
24
|
name: string
|
|
24
25
|
connectTo: string
|
|
25
26
|
targetOrigin: string
|
|
26
|
-
onMessage?:
|
|
27
|
+
onMessage?: {
|
|
28
|
+
[K in TWindowMessage['type']]: (data: Extract<TWindowMessage, {type: K}>['data']) => void
|
|
29
|
+
}
|
|
30
|
+
heartbeat?: boolean
|
|
31
|
+
onStatus?: (status: Status) => void
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/**
|
|
@@ -45,65 +50,56 @@ export function useFrameConnection<
|
|
|
45
50
|
TFrameMessage extends FrameMessage,
|
|
46
51
|
TWindowMessage extends WindowMessage,
|
|
47
52
|
>(options: UseFrameConnectionOptions<TWindowMessage>): FrameConnection<TFrameMessage> {
|
|
48
|
-
const {onMessage, targetOrigin, name, connectTo} = options
|
|
53
|
+
const {onMessage, targetOrigin, name, connectTo, heartbeat, onStatus} = options
|
|
49
54
|
const instance = useSanityInstance()
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
() => getOrCreateController(instance, targetOrigin),
|
|
53
|
-
[instance, targetOrigin],
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
const channel = useMemo(
|
|
57
|
-
() =>
|
|
58
|
-
getOrCreateChannel(instance, {
|
|
59
|
-
name,
|
|
60
|
-
connectTo,
|
|
61
|
-
}),
|
|
62
|
-
[instance, name, connectTo],
|
|
63
|
-
)
|
|
55
|
+
const controllerRef = useRef<Controller | null>(null)
|
|
56
|
+
const channelRef = useRef<ChannelInstance<TFrameMessage, TWindowMessage> | null>(null)
|
|
64
57
|
|
|
65
58
|
useEffect(() => {
|
|
66
|
-
|
|
59
|
+
const controller = getOrCreateController(instance, targetOrigin)
|
|
60
|
+
const channel = getOrCreateChannel(instance, {name, connectTo, heartbeat})
|
|
61
|
+
controllerRef.current = controller
|
|
62
|
+
channelRef.current = channel
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Object.entries(onMessage).forEach(([type, handler]) => {
|
|
71
|
-
const unsubscribe = channel.on(type, handler)
|
|
72
|
-
unsubscribers.push(unsubscribe)
|
|
64
|
+
channel.onStatus((event) => {
|
|
65
|
+
onStatus?.(event.status)
|
|
73
66
|
})
|
|
74
67
|
|
|
68
|
+
const messageUnsubscribers: Array<() => void> = []
|
|
69
|
+
|
|
70
|
+
if (onMessage) {
|
|
71
|
+
Object.entries(onMessage).forEach(([type, handler]) => {
|
|
72
|
+
const unsubscribe = channel.on(type, handler as FrameMessageHandler<TWindowMessage>)
|
|
73
|
+
messageUnsubscribers.push(unsubscribe)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
75
77
|
return () => {
|
|
76
|
-
|
|
78
|
+
// Clean up all subscriptions and stop controller/channel
|
|
79
|
+
messageUnsubscribers.forEach((unsub) => unsub())
|
|
80
|
+
releaseChannel(instance, name)
|
|
81
|
+
channelRef.current = null
|
|
82
|
+
controllerRef.current = null
|
|
77
83
|
}
|
|
78
|
-
}, [
|
|
84
|
+
}, [targetOrigin, name, connectTo, heartbeat, onMessage, instance, onStatus])
|
|
79
85
|
|
|
80
|
-
const connect = useCallback(
|
|
81
|
-
(frameWindow
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
},
|
|
87
|
-
[controller],
|
|
88
|
-
)
|
|
86
|
+
const connect = useCallback((frameWindow: Window) => {
|
|
87
|
+
const removeTarget = controllerRef.current?.addTarget(frameWindow)
|
|
88
|
+
return () => {
|
|
89
|
+
removeTarget?.()
|
|
90
|
+
}
|
|
91
|
+
}, [])
|
|
89
92
|
|
|
90
93
|
const sendMessage = useCallback(
|
|
91
94
|
<T extends TFrameMessage['type']>(
|
|
92
95
|
type: T,
|
|
93
96
|
data?: Extract<TFrameMessage, {type: T}>['data'],
|
|
94
97
|
) => {
|
|
95
|
-
|
|
98
|
+
channelRef.current?.post(type, data)
|
|
96
99
|
},
|
|
97
|
-
[
|
|
100
|
+
[],
|
|
98
101
|
)
|
|
99
102
|
|
|
100
|
-
// cleanup channel on unmount
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
return () => {
|
|
103
|
-
releaseChannel(instance, name)
|
|
104
|
-
}
|
|
105
|
-
}, [name, instance])
|
|
106
|
-
|
|
107
103
|
return {
|
|
108
104
|
connect,
|
|
109
105
|
sendMessage,
|
|
@@ -0,0 +1,106 @@
|
|
|
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 {useManageFavorite} from './useManageFavorite'
|
|
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('useManageFavorite', () => {
|
|
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 default states', () => {
|
|
45
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
46
|
+
|
|
47
|
+
expect(result.current.isFavorited).toBe(false)
|
|
48
|
+
expect(result.current.isConnected).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should handle favorite action', () => {
|
|
52
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
53
|
+
|
|
54
|
+
act(() => {
|
|
55
|
+
result.current.favorite()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
expect(node.post).toHaveBeenCalledWith('core/v1/events/favorite', {
|
|
59
|
+
documentId: 'mock-id',
|
|
60
|
+
documentType: 'mock-type',
|
|
61
|
+
eventType: 'added',
|
|
62
|
+
})
|
|
63
|
+
expect(result.current.isFavorited).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should handle unfavorite action', () => {
|
|
67
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
result.current.unfavorite()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
expect(node.post).toHaveBeenCalledWith('core/v1/events/favorite', {
|
|
74
|
+
documentId: 'mock-id',
|
|
75
|
+
documentType: 'mock-type',
|
|
76
|
+
eventType: 'removed',
|
|
77
|
+
})
|
|
78
|
+
expect(result.current.isFavorited).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should throw error during favorite/unfavorite actions', () => {
|
|
82
|
+
const errorMessage = 'Failed to update favorite status'
|
|
83
|
+
|
|
84
|
+
vi.mocked(node.post).mockImplementationOnce(() => {
|
|
85
|
+
throw new Error(errorMessage)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
89
|
+
|
|
90
|
+
act(() => {
|
|
91
|
+
expect(() => result.current.favorite()).toThrow(errorMessage)
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should update connection status', () => {
|
|
96
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
97
|
+
|
|
98
|
+
expect(result.current.isConnected).toBe(false)
|
|
99
|
+
|
|
100
|
+
act(() => {
|
|
101
|
+
statusCallback?.('connected')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
expect(result.current.isConnected).toBe(true)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {type Status} from '@sanity/comlink'
|
|
2
|
+
import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
3
|
+
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
4
|
+
import {useCallback, useState} from 'react'
|
|
5
|
+
|
|
6
|
+
import {useWindowConnection} from './useWindowConnection'
|
|
7
|
+
|
|
8
|
+
// should we import this whole type from the message protocol?
|
|
9
|
+
|
|
10
|
+
interface ManageFavorite {
|
|
11
|
+
favorite: () => void
|
|
12
|
+
unfavorite: () => void
|
|
13
|
+
isFavorited: boolean
|
|
14
|
+
isConnected: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @beta
|
|
19
|
+
*
|
|
20
|
+
* ## useManageFavorite
|
|
21
|
+
* This hook provides functionality to add and remove documents from favorites,
|
|
22
|
+
* and tracks the current favorite status of the document.
|
|
23
|
+
* @category Core UI Communication
|
|
24
|
+
* @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
|
|
25
|
+
* @returns An object containing:
|
|
26
|
+
* - `favorite` - Function to add document to favorites
|
|
27
|
+
* - `unfavorite` - Function to remove document from favorites
|
|
28
|
+
* - `isFavorited` - Boolean indicating if document is currently favorited
|
|
29
|
+
* - `isConnected` - Boolean indicating if connection to Core UI is established
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* function MyDocumentAction(props: DocumentActionProps) {
|
|
34
|
+
* const {_id, _type} = props
|
|
35
|
+
* const {favorite, unfavorite, isFavorited, isConnected} = useManageFavorite({
|
|
36
|
+
* _id,
|
|
37
|
+
* _type
|
|
38
|
+
* })
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <Button
|
|
42
|
+
* disabled={!isConnected}
|
|
43
|
+
* onClick={() => isFavorited ? unfavorite() : favorite()}
|
|
44
|
+
* text={isFavorited ? 'Remove from favorites' : 'Add to favorites'}
|
|
45
|
+
* />
|
|
46
|
+
* )
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function useManageFavorite({_id, _type}: DocumentHandle): ManageFavorite {
|
|
51
|
+
const [isFavorited, setIsFavorited] = useState(false) // should load this from a comlink fetch
|
|
52
|
+
const [status, setStatus] = useState<Status>('idle')
|
|
53
|
+
const {sendMessage} = useWindowConnection<Events.FavoriteMessage, FrameMessage>({
|
|
54
|
+
name: SDK_NODE_NAME,
|
|
55
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
56
|
+
onStatus: setStatus,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const handleFavoriteAction = useCallback(
|
|
60
|
+
(action: 'added' | 'removed', setFavoriteState: boolean) => {
|
|
61
|
+
if (!_id || !_type) return
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const message: Events.FavoriteMessage = {
|
|
65
|
+
type: 'core/v1/events/favorite',
|
|
66
|
+
data: {
|
|
67
|
+
eventType: action,
|
|
68
|
+
documentId: _id,
|
|
69
|
+
documentType: _type,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sendMessage(message.type, message.data)
|
|
74
|
+
setIsFavorited(setFavoriteState)
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const error = err instanceof Error ? err : new Error('Failed to update favorite status')
|
|
77
|
+
// eslint-disable-next-line no-console
|
|
78
|
+
console.error(
|
|
79
|
+
`Failed to ${action === 'added' ? 'favorite' : 'unfavorite'} document:`,
|
|
80
|
+
error,
|
|
81
|
+
)
|
|
82
|
+
throw error
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[_id, _type, sendMessage],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const favorite = useCallback(() => handleFavoriteAction('added', true), [handleFavoriteAction])
|
|
89
|
+
|
|
90
|
+
const unfavorite = useCallback(
|
|
91
|
+
() => handleFavoriteAction('removed', false),
|
|
92
|
+
[handleFavoriteAction],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
favorite,
|
|
97
|
+
unfavorite,
|
|
98
|
+
isFavorited,
|
|
99
|
+
isConnected: status === 'connected',
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -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,79 @@
|
|
|
1
|
+
import {type Status} from '@sanity/comlink'
|
|
2
|
+
import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
3
|
+
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
4
|
+
import {useCallback, useState} from 'react'
|
|
5
|
+
|
|
6
|
+
import {useWindowConnection} from './useWindowConnection'
|
|
7
|
+
|
|
8
|
+
interface DocumentInteractionHistory {
|
|
9
|
+
recordEvent: (eventType: 'viewed' | 'edited' | 'created' | 'deleted') => void
|
|
10
|
+
isConnected: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @public
|
|
15
|
+
* Hook for managing document interaction history in Sanity Studio.
|
|
16
|
+
* This hook provides functionality to record document interactions.
|
|
17
|
+
* @category History
|
|
18
|
+
* @param documentHandle - The document handle containing document ID and type, like `{_id: '123', _type: 'book'}`
|
|
19
|
+
* @returns An object containing:
|
|
20
|
+
* - `recordEvent` - Function to record document interactions
|
|
21
|
+
* - `isConnected` - Boolean indicating if connection to Studio is established
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* function MyDocumentAction(props: DocumentActionProps) {
|
|
26
|
+
* const {_id, _type} = props
|
|
27
|
+
* const {recordEvent, isConnected} = useRecordDocumentHistoryEvent({
|
|
28
|
+
* _id,
|
|
29
|
+
* _type
|
|
30
|
+
* })
|
|
31
|
+
*
|
|
32
|
+
* return (
|
|
33
|
+
* <Button
|
|
34
|
+
* disabled={!isConnected}
|
|
35
|
+
* onClick={() => recordEvent('viewed')}
|
|
36
|
+
* text={'Viewed'}
|
|
37
|
+
* />
|
|
38
|
+
* )
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function useRecordDocumentHistoryEvent({
|
|
43
|
+
_id,
|
|
44
|
+
_type,
|
|
45
|
+
}: DocumentHandle): DocumentInteractionHistory {
|
|
46
|
+
const [status, setStatus] = useState<Status>('idle')
|
|
47
|
+
const {sendMessage} = useWindowConnection<Events.HistoryMessage, FrameMessage>({
|
|
48
|
+
name: SDK_NODE_NAME,
|
|
49
|
+
connectTo: SDK_CHANNEL_NAME,
|
|
50
|
+
onStatus: setStatus,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const recordEvent = useCallback(
|
|
54
|
+
(eventType: 'viewed' | 'edited' | 'created' | 'deleted') => {
|
|
55
|
+
try {
|
|
56
|
+
const message: Events.HistoryMessage = {
|
|
57
|
+
type: 'core/v1/events/history',
|
|
58
|
+
data: {
|
|
59
|
+
eventType,
|
|
60
|
+
documentId: _id,
|
|
61
|
+
documentType: _type,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sendMessage(message.type, message.data)
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.error('Failed to record history event:', error)
|
|
69
|
+
throw error
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[_id, _type, sendMessage],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
recordEvent,
|
|
77
|
+
isConnected: status === 'connected',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -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,65 @@ 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 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
|
+
|
|
47
88
|
it('should register message handlers', () => {
|
|
48
89
|
const mockHandler = vi.fn()
|
|
49
90
|
const mockData = {someData: 'test'}
|