diff --git a/packages/react/src/_exports/sdk-react.ts b/packages/react/src/_exports/sdk-react.ts index 88c38a77..5cfb573c 100644 --- a/packages/react/src/_exports/sdk-react.ts +++ b/packages/react/src/_exports/sdk-react.ts @@ -35,6 +35,7 @@ export { useNavigateToStudioDocument, } from '../hooks/dashboard/useNavigateToStudioDocument' export {useRecordDocumentHistoryEvent} from '../hooks/dashboard/useRecordDocumentHistoryEvent' +export {useSendIntent} from '../hooks/dashboard/useSendIntent' export {useStudioWorkspacesByProjectIdDataset} from '../hooks/dashboard/useStudioWorkspacesByProjectIdDataset' export {useDatasets} from '../hooks/datasets/useDatasets' export {useApplyDocumentActions} from '../hooks/document/useApplyDocumentActions' diff --git a/packages/react/src/hooks/dashboard/useSendIntent.test.ts b/packages/react/src/hooks/dashboard/useSendIntent.test.ts new file mode 100644 index 00000000..d3e0417c --- /dev/null +++ b/packages/react/src/hooks/dashboard/useSendIntent.test.ts @@ -0,0 +1,121 @@ +import {type DocumentHandle} from '@sanity/sdk' +import {renderHook} from '@testing-library/react' +import {beforeEach, describe, expect, it, vi} from 'vitest' + +import {useSendIntent} from './useSendIntent' + +// Mock the useWindowConnection hook +const mockSendMessage = vi.fn() +vi.mock('../comlink/useWindowConnection', () => ({ + useWindowConnection: vi.fn(() => ({ + sendMessage: mockSendMessage, + })), +})) + +describe('useSendIntent', () => { + const mockDocumentHandle: DocumentHandle = { + documentId: 'test-document-id', + documentType: 'test-document-type', + projectId: 'test-project-id', + dataset: 'test-dataset', + } + + beforeEach(() => { + vi.clearAllMocks() + // Reset mock implementation to default behavior + mockSendMessage.mockImplementation(() => {}) + }) + + it('should return sendIntent function', () => { + const {result} = renderHook(() => useSendIntent({documentHandle: mockDocumentHandle})) + + expect(result.current).toEqual({ + sendIntent: expect.any(Function), + }) + }) + + it('should send intent message when sendIntent is called', () => { + const {result} = renderHook(() => useSendIntent({documentHandle: mockDocumentHandle})) + + result.current.sendIntent() + + expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/send-intent', { + document: { + id: 'test-document-id', + type: 'test-document-type', + }, + resource: { + id: 'test-project-id.test-dataset', + }, + }) + }) + + it('should handle errors gracefully', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + mockSendMessage.mockImplementation(() => { + throw new Error('Test error') + }) + + const {result} = renderHook(() => useSendIntent({documentHandle: mockDocumentHandle})) + + expect(() => result.current.sendIntent()).toThrow('Test error') + expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to send intent:', expect.any(Error)) + + consoleErrorSpy.mockRestore() + }) + + it('should use memoized sendIntent function', () => { + const {result, rerender} = renderHook(({params}) => useSendIntent(params), { + initialProps: {params: {documentHandle: mockDocumentHandle}}, + }) + + const firstSendIntent = result.current.sendIntent + + // Rerender with the same params + rerender({params: {documentHandle: mockDocumentHandle}}) + + expect(result.current.sendIntent).toBe(firstSendIntent) + }) + + it('should create new sendIntent function when documentHandle changes', () => { + const {result, rerender} = renderHook(({params}) => useSendIntent(params), { + initialProps: {params: {documentHandle: mockDocumentHandle}}, + }) + + const firstSendIntent = result.current.sendIntent + + const newDocumentHandle: DocumentHandle = { + documentId: 'new-document-id', + documentType: 'new-document-type', + projectId: 'new-project-id', + dataset: 'new-dataset', + } + + rerender({params: {documentHandle: newDocumentHandle}}) + + expect(result.current.sendIntent).not.toBe(firstSendIntent) + }) + + it('should send intent message with params when provided', () => { + const intentParams = {view: 'editor', tab: 'content'} + const {result} = renderHook(() => + useSendIntent({ + documentHandle: mockDocumentHandle, + params: intentParams, + }), + ) + + result.current.sendIntent() + + expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/send-intent', { + document: { + id: 'test-document-id', + type: 'test-document-type', + }, + resource: { + id: 'test-project-id.test-dataset', + }, + params: intentParams, + }) + }) +}) diff --git a/packages/react/src/hooks/dashboard/useSendIntent.ts b/packages/react/src/hooks/dashboard/useSendIntent.ts new file mode 100644 index 00000000..03096c09 --- /dev/null +++ b/packages/react/src/hooks/dashboard/useSendIntent.ts @@ -0,0 +1,132 @@ +import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol' +import {type DocumentHandle, type FrameMessage} from '@sanity/sdk' +import {useCallback} from 'react' + +import {useWindowConnection} from '../comlink/useWindowConnection' + +/** + * Message type for sending intents to the dashboard + * @internal + */ +export interface IntentMessage { + type: 'dashboard/v1/events/intents/send-intent' + data: { + intentName?: string + document: { + id: string + type: string + } + resource?: { + id: string + } + params?: Record + } +} + +/** + * Return type for the useSendIntent hook + * @public + */ +interface SendIntent { + sendIntent: () => void +} + +/** + * Parameters for the useSendIntent hook + * @public + */ +interface UseSendIntentParams { + intentName?: string + documentHandle: DocumentHandle + params?: Record +} + +/** + * @public + * + * A hook for sending intent messages to the Dashboard with a document handle. + * This allows applications to signal intent for specific documents to the Dashboard. + * + * @param params - Object containing: + * - `intentName` - Optional specific name of the intent to send + * - `documentHandle` - The document handle containing document ID, type, project ID and dataset, like `{documentId: '123', documentType: 'book', projectId: 'abc123', dataset: 'production'}` + * - `params` - Optional parameters to include in the intent + * @returns An object containing: + * - `sendIntent` - Function to send the intent message + * + * @example + * ```tsx + * import {useSendIntent} from '@sanity/sdk-react' + * import {Button} from '@sanity/ui' + * import {Suspense} from 'react' + * + * function SendIntentButton({documentId, documentType, projectId, dataset}) { + * const {sendIntent} = useSendIntent({ + * intentName: 'edit-document', + * documentHandle: {documentId, documentType, projectId, dataset}, + * params: {view: 'editor'} + * }) + * + * return ( + *