@sanity/sdk-react 0.0.0-alpha.8 → 0.0.0-chore-react-18-compat.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 +4811 -2
- package/dist/index.js +1069 -2
- package/dist/index.js.map +1 -1
- package/package.json +23 -45
- 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 +4 -4
- 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.test.tsx +3 -3
- package/src/components/auth/LoginCallback.tsx +8 -11
- 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 +27 -20
- 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} +6 -6
- package/src/hooks/auth/useLogOut.test.tsx +2 -2
- package/src/hooks/client/useClient.ts +9 -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 +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 +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.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 +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 +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/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/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 -135
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {type ChannelInstance, type Controller} from '@sanity/comlink'
|
|
1
|
+
import {type ChannelInstance, type Controller, type Status} from '@sanity/comlink'
|
|
2
2
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
3
|
|
|
4
|
-
import {renderHook} from '../../../test/test-utils'
|
|
4
|
+
import {act, renderHook} from '../../../test/test-utils'
|
|
5
5
|
import {useFrameConnection} from './useFrameConnection'
|
|
6
6
|
|
|
7
7
|
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
@@ -30,18 +30,25 @@ interface TestNodeMessage {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function createMockChannel() {
|
|
34
|
-
return {
|
|
35
|
-
on: vi.fn(() => () => {}),
|
|
36
|
-
post: vi.fn(),
|
|
37
|
-
stop: vi.fn(),
|
|
38
|
-
} as unknown as ChannelInstance<TestControllerMessage, TestNodeMessage>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
33
|
describe('useFrameController', () => {
|
|
42
34
|
let channel: ChannelInstance<TestControllerMessage, TestNodeMessage>
|
|
43
35
|
let controller: Controller
|
|
44
36
|
let removeTargetMock: ReturnType<typeof vi.fn>
|
|
37
|
+
let statusCallback:
|
|
38
|
+
| (({status, connection}: {status: Status; connection: string}) => void)
|
|
39
|
+
| null = null
|
|
40
|
+
|
|
41
|
+
function createMockChannel() {
|
|
42
|
+
return {
|
|
43
|
+
on: vi.fn(() => () => {}),
|
|
44
|
+
post: vi.fn(),
|
|
45
|
+
stop: vi.fn(),
|
|
46
|
+
onStatus: vi.fn((callback) => {
|
|
47
|
+
statusCallback = callback
|
|
48
|
+
return () => {}
|
|
49
|
+
}),
|
|
50
|
+
} as unknown as ChannelInstance<TestControllerMessage, TestNodeMessage>
|
|
51
|
+
}
|
|
45
52
|
|
|
46
53
|
beforeEach(() => {
|
|
47
54
|
channel = createMockChannel()
|
|
@@ -54,6 +61,44 @@ describe('useFrameController', () => {
|
|
|
54
61
|
vi.mocked(getOrCreateController).mockReturnValue(controller)
|
|
55
62
|
})
|
|
56
63
|
|
|
64
|
+
it('should call onStatus callback when status changes', () => {
|
|
65
|
+
const onStatusMock = vi.fn()
|
|
66
|
+
renderHook(() =>
|
|
67
|
+
useFrameConnection({
|
|
68
|
+
name: 'test',
|
|
69
|
+
connectTo: 'iframe',
|
|
70
|
+
targetOrigin: '*',
|
|
71
|
+
onStatus: onStatusMock,
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
act(() => {
|
|
76
|
+
statusCallback?.({status: 'connected', connection: 'test'})
|
|
77
|
+
})
|
|
78
|
+
expect(onStatusMock).toHaveBeenCalledWith('connected')
|
|
79
|
+
|
|
80
|
+
act(() => {
|
|
81
|
+
statusCallback?.({status: 'disconnected', connection: 'test'})
|
|
82
|
+
})
|
|
83
|
+
expect(onStatusMock).toHaveBeenCalledWith('disconnected')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should not throw if onStatus is not provided', () => {
|
|
87
|
+
renderHook(() =>
|
|
88
|
+
useFrameConnection({
|
|
89
|
+
name: 'test',
|
|
90
|
+
connectTo: 'iframe',
|
|
91
|
+
targetOrigin: '*',
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(() => {
|
|
96
|
+
act(() => {
|
|
97
|
+
statusCallback?.({status: 'connected', connection: 'test'})
|
|
98
|
+
})
|
|
99
|
+
}).not.toThrow()
|
|
100
|
+
})
|
|
101
|
+
|
|
57
102
|
it('should register and execute message handlers', () => {
|
|
58
103
|
const mockHandler = vi.fn()
|
|
59
104
|
const mockData = {someData: 'test'}
|
|
@@ -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,111 @@
|
|
|
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
|
+
documentId: 'mock-id',
|
|
23
|
+
documentType: 'mock-type',
|
|
24
|
+
resourceType: 'studio' as const,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createMockNode() {
|
|
28
|
+
return {
|
|
29
|
+
on: vi.fn(() => () => {}),
|
|
30
|
+
post: vi.fn(),
|
|
31
|
+
stop: vi.fn(),
|
|
32
|
+
onStatus: vi.fn((callback) => {
|
|
33
|
+
statusCallback = callback
|
|
34
|
+
return () => {}
|
|
35
|
+
}),
|
|
36
|
+
} as unknown as Node<Message, Message>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
statusCallback = null
|
|
41
|
+
node = createMockNode()
|
|
42
|
+
vi.mocked(getOrCreateNode).mockReturnValue(node)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should initialize with default states', () => {
|
|
46
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
47
|
+
|
|
48
|
+
expect(result.current.isFavorited).toBe(false)
|
|
49
|
+
expect(result.current.isConnected).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should handle favorite action', () => {
|
|
53
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
54
|
+
|
|
55
|
+
act(() => {
|
|
56
|
+
result.current.favorite()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
expect(node.post).toHaveBeenCalledWith('dashboard/v1/events/favorite/mutate', {
|
|
60
|
+
documentId: 'mock-id',
|
|
61
|
+
documentType: 'mock-type',
|
|
62
|
+
eventType: 'added',
|
|
63
|
+
resourceType: 'studio',
|
|
64
|
+
resourceId: undefined,
|
|
65
|
+
})
|
|
66
|
+
expect(result.current.isFavorited).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should handle unfavorite action', () => {
|
|
70
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
71
|
+
|
|
72
|
+
act(() => {
|
|
73
|
+
result.current.unfavorite()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(node.post).toHaveBeenCalledWith('dashboard/v1/events/favorite/mutate', {
|
|
77
|
+
documentId: 'mock-id',
|
|
78
|
+
documentType: 'mock-type',
|
|
79
|
+
eventType: 'removed',
|
|
80
|
+
resourceType: 'studio',
|
|
81
|
+
resourceId: undefined,
|
|
82
|
+
})
|
|
83
|
+
expect(result.current.isFavorited).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should throw error during favorite/unfavorite actions', () => {
|
|
87
|
+
const errorMessage = 'Failed to update favorite status'
|
|
88
|
+
|
|
89
|
+
vi.mocked(node.post).mockImplementationOnce(() => {
|
|
90
|
+
throw new Error(errorMessage)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
94
|
+
|
|
95
|
+
act(() => {
|
|
96
|
+
expect(() => result.current.favorite()).toThrow(errorMessage)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should update connection status', () => {
|
|
101
|
+
const {result} = renderHook(() => useManageFavorite(mockDocumentHandle))
|
|
102
|
+
|
|
103
|
+
expect(result.current.isConnected).toBe(false)
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
statusCallback?.('connected')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
expect(result.current.isConnected).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -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
|
+
}
|