@sanity/sdk-react 0.0.0-alpha.2 → 0.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -67
- package/dist/index.d.ts +4811 -2
- package/dist/index.js +1069 -2
- package/dist/index.js.map +1 -1
- package/package.json +27 -58
- package/src/_exports/index.ts +66 -10
- package/src/components/Login/LoginLinks.test.tsx +4 -14
- package/src/components/Login/LoginLinks.tsx +16 -31
- package/src/components/SDKProvider.test.tsx +79 -0
- package/src/components/SDKProvider.tsx +42 -0
- package/src/components/SanityApp.test.tsx +156 -0
- package/src/components/SanityApp.tsx +90 -0
- package/src/components/auth/AuthBoundary.test.tsx +6 -19
- package/src/components/auth/AuthBoundary.tsx +20 -4
- package/src/components/auth/Login.test.tsx +2 -16
- package/src/components/auth/Login.tsx +11 -30
- package/src/components/auth/LoginCallback.test.tsx +5 -20
- package/src/components/auth/LoginCallback.tsx +9 -14
- package/src/components/auth/LoginError.test.tsx +2 -17
- package/src/components/auth/LoginError.tsx +11 -16
- package/src/components/auth/LoginFooter.test.tsx +2 -16
- package/src/components/auth/LoginFooter.tsx +8 -24
- package/src/components/auth/LoginLayout.test.tsx +2 -16
- package/src/components/auth/LoginLayout.tsx +8 -38
- package/src/components/auth/authTestHelpers.tsx +11 -0
- package/src/components/utils.ts +22 -0
- package/src/context/SanityInstanceContext.ts +4 -0
- package/src/{components/context → context}/SanityProvider.test.tsx +2 -2
- package/src/context/SanityProvider.tsx +50 -0
- package/src/hooks/_synchronous-groq-js.mjs +4 -0
- package/src/hooks/auth/useAuthState.tsx +4 -5
- package/src/hooks/auth/useAuthToken.tsx +1 -1
- package/src/hooks/auth/useCurrentUser.tsx +28 -4
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
- package/src/hooks/auth/useDashboardOrganizationId.tsx +29 -0
- package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
- package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
- package/src/hooks/auth/useLogOut.test.tsx +2 -2
- package/src/hooks/auth/useLogOut.tsx +1 -1
- package/src/hooks/auth/useLoginUrls.tsx +1 -0
- package/src/hooks/client/useClient.ts +9 -30
- package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
- package/src/hooks/comlink/useFrameConnection.ts +107 -0
- package/src/hooks/comlink/useManageFavorite.test.ts +111 -0
- package/src/hooks/comlink/useManageFavorite.ts +130 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +81 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +106 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
- package/src/hooks/comlink/useWindowConnection.ts +122 -0
- package/src/hooks/context/useSanityInstance.test.tsx +2 -2
- package/src/hooks/context/useSanityInstance.ts +24 -8
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +178 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +123 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.test.tsx +278 -0
- package/src/hooks/dashboard/useStudioWorkspacesByResourceId.ts +92 -0
- package/src/hooks/datasets/useDatasets.ts +40 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +25 -0
- package/src/hooks/document/useApplyDocumentActions.ts +75 -0
- package/src/hooks/document/useDocument.test.ts +81 -0
- package/src/hooks/document/useDocument.ts +107 -0
- package/src/hooks/document/useDocumentEvent.test.ts +63 -0
- package/src/hooks/document/useDocumentEvent.ts +54 -0
- package/src/hooks/document/useDocumentPermissions.ts +84 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +16 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +33 -0
- package/src/hooks/document/useEditDocument.test.ts +179 -0
- package/src/hooks/document/useEditDocument.ts +195 -0
- package/src/hooks/documents/useDocuments.test.tsx +152 -0
- package/src/hooks/documents/useDocuments.ts +174 -0
- package/src/hooks/helpers/createCallbackHook.tsx +3 -2
- package/src/hooks/helpers/createStateSourceHook.test.tsx +66 -0
- package/src/hooks/helpers/createStateSourceHook.tsx +29 -10
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +259 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +290 -0
- package/src/hooks/preview/usePreview.test.tsx +19 -10
- package/src/hooks/preview/usePreview.tsx +67 -13
- package/src/hooks/projection/useProjection.test.tsx +218 -0
- package/src/hooks/projection/useProjection.ts +147 -0
- package/src/hooks/projects/useProject.ts +48 -0
- package/src/hooks/projects/useProjects.ts +45 -0
- package/src/hooks/query/useQuery.test.tsx +188 -0
- package/src/hooks/query/useQuery.ts +103 -0
- package/src/hooks/users/useUsers.test.ts +163 -0
- package/src/hooks/users/useUsers.ts +107 -0
- package/src/utils/getEnv.ts +21 -0
- package/src/version.ts +8 -0
- package/src/vite-env.d.ts +10 -0
- package/dist/_chunks-es/useLogOut.js +0 -44
- package/dist/_chunks-es/useLogOut.js.map +0 -1
- package/dist/assets/bundle-CcAyERuZ.css +0 -11
- package/dist/components.d.ts +0 -257
- package/dist/components.js +0 -316
- package/dist/components.js.map +0 -1
- package/dist/hooks.d.ts +0 -187
- package/dist/hooks.js +0 -81
- package/dist/hooks.js.map +0 -1
- package/src/_exports/components.ts +0 -13
- package/src/_exports/hooks.ts +0 -9
- package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
- package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
- package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
- package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
- package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
- package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
- package/src/components/context/SanityProvider.tsx +0 -42
- package/src/css/css.config.js +0 -220
- package/src/css/paramour.css +0 -2347
- package/src/css/styles.css +0 -11
- package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
- package/src/hooks/client/useClient.test.tsx +0 -130
- package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
- package/src/hooks/documentCollection/useDocuments.ts +0 -87
|
@@ -1,10 +1,34 @@
|
|
|
1
|
-
import {getCurrentUserState} from '@sanity/sdk'
|
|
1
|
+
import {type CurrentUser, getCurrentUserState} from '@sanity/sdk'
|
|
2
2
|
|
|
3
3
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
4
4
|
|
|
5
|
+
type UseCurrentUser = {
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*
|
|
9
|
+
* Provides the currently authenticated user’s profile information.
|
|
10
|
+
*
|
|
11
|
+
* @category Users
|
|
12
|
+
* @returns The current user data
|
|
13
|
+
*
|
|
14
|
+
* @example Rendering a basic user profile
|
|
15
|
+
* ```
|
|
16
|
+
* const user = useCurrentUser()
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <figure>
|
|
20
|
+
* <img src={user?.profileImage} alt=`Profile image for ${user?.name}` />
|
|
21
|
+
* <h2>{user?.name}</h2>
|
|
22
|
+
* </figure>
|
|
23
|
+
* )
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
(): CurrentUser | null
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
/**
|
|
6
|
-
* Hook to get the currently logged in user
|
|
7
30
|
* @public
|
|
8
|
-
* @
|
|
31
|
+
* @function
|
|
32
|
+
* @TODO This should not return null — users of a custom app will always be authenticated via Core
|
|
9
33
|
*/
|
|
10
|
-
export const useCurrentUser = createStateSourceHook(getCurrentUserState)
|
|
34
|
+
export const useCurrentUser: UseCurrentUser = createStateSourceHook(getCurrentUserState)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {createSanityInstance, getDashboardOrganizationId} from '@sanity/sdk'
|
|
2
|
+
import {renderHook} from '@testing-library/react'
|
|
3
|
+
import {throwError} from 'rxjs'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {useDashboardOrganizationId} from './useDashboardOrganizationId'
|
|
7
|
+
|
|
8
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
9
|
+
useSanityInstance: vi.fn().mockReturnValue(createSanityInstance({projectId: 'p', dataset: 'd'})),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal()
|
|
14
|
+
return {...(actual || {}), getDashboardOrganizationId: vi.fn()}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('useDashboardOrganizationId', () => {
|
|
18
|
+
it('should return undefined when no organization ID is set', () => {
|
|
19
|
+
const subscribe = vi.fn()
|
|
20
|
+
vi.mocked(getDashboardOrganizationId).mockReturnValue({
|
|
21
|
+
getCurrent: () => undefined,
|
|
22
|
+
subscribe,
|
|
23
|
+
observable: throwError(() => new Error('Unexpected usage of observable')),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const {result} = renderHook(() => useDashboardOrganizationId())
|
|
27
|
+
expect(result.current).toBeUndefined()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return organization ID when one is set', () => {
|
|
31
|
+
const subscribe = vi.fn()
|
|
32
|
+
const mockOrgId = 'team_123'
|
|
33
|
+
vi.mocked(getDashboardOrganizationId).mockReturnValue({
|
|
34
|
+
getCurrent: () => mockOrgId,
|
|
35
|
+
subscribe,
|
|
36
|
+
observable: throwError(() => new Error('Unexpected usage of observable')),
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const {result} = renderHook(() => useDashboardOrganizationId())
|
|
40
|
+
expect(result.current).toBe(mockOrgId)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {getDashboardOrganizationId} from '@sanity/sdk'
|
|
2
|
+
import {useMemo, useSyncExternalStore} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @remarks
|
|
8
|
+
* A React hook that retrieves the dashboard organization ID that is currently selected in the Sanity Dashboard.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* function DashboardComponent() {
|
|
13
|
+
* const orgId = useDashboardOrganizationId()
|
|
14
|
+
*
|
|
15
|
+
* if (!orgId) return null
|
|
16
|
+
*
|
|
17
|
+
* return <div>Organization ID: {String(orgId)}</div>
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @returns The dashboard organization ID (string | undefined)
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export function useDashboardOrganizationId(): string | undefined {
|
|
25
|
+
const instance = useSanityInstance()
|
|
26
|
+
const {subscribe, getCurrent} = useMemo(() => getDashboardOrganizationId(instance), [instance])
|
|
27
|
+
|
|
28
|
+
return useSyncExternalStore(subscribe, getCurrent)
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {handleAuthCallback} from '@sanity/sdk'
|
|
2
|
+
import {identity} from 'rxjs'
|
|
3
|
+
import {describe, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
6
|
+
|
|
7
|
+
vi.mock('../helpers/createCallbackHook', () => ({createCallbackHook: vi.fn(identity)}))
|
|
8
|
+
vi.mock('@sanity/sdk', () => ({handleAuthCallback: vi.fn()}))
|
|
9
|
+
|
|
10
|
+
describe('useHandleAuthCallback', () => {
|
|
11
|
+
it('calls `createCallbackHook` with `handleAuthCallback`', async () => {
|
|
12
|
+
const {useHandleAuthCallback} = await import('./useHandleAuthCallback')
|
|
13
|
+
expect(createCallbackHook).toHaveBeenCalledWith(handleAuthCallback)
|
|
14
|
+
expect(useHandleAuthCallback).toBe(handleAuthCallback)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {handleAuthCallback} from '@sanity/sdk'
|
|
2
2
|
|
|
3
3
|
import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
+
* @internal
|
|
6
7
|
* A React hook that returns a function for handling authentication callbacks.
|
|
7
8
|
*
|
|
8
9
|
* @remarks
|
|
9
10
|
* This hook provides access to the authentication store's callback handler,
|
|
10
11
|
* which processes auth redirects by extracting the session ID and fetching the
|
|
11
12
|
* authentication token. If fetching the long-lived token is successful,
|
|
12
|
-
* `
|
|
13
|
+
* `handleAuthCallback` will return a Promise that resolves a new location that
|
|
13
14
|
* removes the short-lived token from the URL. Use this in combination with
|
|
14
15
|
* `history.replaceState` or your own router's `replace` function to update the
|
|
15
16
|
* current location without triggering a reload.
|
|
@@ -17,13 +18,13 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
|
17
18
|
* @example
|
|
18
19
|
* ```tsx
|
|
19
20
|
* function AuthCallback() {
|
|
20
|
-
* const
|
|
21
|
+
* const handleAuthCallback = useHandleAuthCallback()
|
|
21
22
|
* const router = useRouter() // Example router
|
|
22
23
|
*
|
|
23
24
|
* useEffect(() => {
|
|
24
25
|
* async function processCallback() {
|
|
25
26
|
* // Handle the callback and get the cleaned URL
|
|
26
|
-
* const newUrl = await
|
|
27
|
+
* const newUrl = await handleAuthCallback(window.location.href)
|
|
27
28
|
*
|
|
28
29
|
* if (newUrl) {
|
|
29
30
|
* // Replace URL without triggering navigation
|
|
@@ -32,7 +33,7 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
|
32
33
|
* }
|
|
33
34
|
*
|
|
34
35
|
* processCallback().catch(console.error)
|
|
35
|
-
* }, [
|
|
36
|
+
* }, [handleAuthCallback, router])
|
|
36
37
|
*
|
|
37
38
|
* return <div>Completing login...</div>
|
|
38
39
|
* }
|
|
@@ -41,4 +42,4 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
|
41
42
|
* @returns A callback handler function that processes OAuth redirects
|
|
42
43
|
* @public
|
|
43
44
|
*/
|
|
44
|
-
export const
|
|
45
|
+
export const useHandleAuthCallback = createCallbackHook(handleAuthCallback)
|
|
@@ -7,8 +7,8 @@ import {createCallbackHook} from '../helpers/createCallbackHook'
|
|
|
7
7
|
vi.mock('../helpers/createCallbackHook', () => ({createCallbackHook: vi.fn(identity)}))
|
|
8
8
|
vi.mock('@sanity/sdk', () => ({logout: vi.fn()}))
|
|
9
9
|
|
|
10
|
-
describe('
|
|
11
|
-
it('calls `createCallbackHook` with `
|
|
10
|
+
describe('useLogOut', () => {
|
|
11
|
+
it('calls `createCallbackHook` with `logout`', async () => {
|
|
12
12
|
const {useLogOut} = await import('./useLogOut')
|
|
13
13
|
expect(createCallbackHook).toHaveBeenCalledWith(logout)
|
|
14
14
|
expect(useLogOut).toBe(logout)
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {type ClientOptions, getClient, getSubscribableClient} from '@sanity/sdk'
|
|
3
|
-
import {useCallback, useSyncExternalStore} from 'react'
|
|
1
|
+
import {getClientState} from '@sanity/sdk'
|
|
4
2
|
|
|
5
|
-
import {
|
|
3
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* A React hook that provides a client that subscribes to changes in your application,
|
|
@@ -12,12 +10,13 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
12
10
|
* The hook uses `useSyncExternalStore` to safely subscribe to changes
|
|
13
11
|
* and ensure consistency between server and client rendering.
|
|
14
12
|
*
|
|
13
|
+
* @category Platform
|
|
15
14
|
* @returns A Sanity client
|
|
16
15
|
*
|
|
17
16
|
* @example
|
|
18
17
|
* ```tsx
|
|
19
18
|
* function MyComponent() {
|
|
20
|
-
* const client = useClient()
|
|
19
|
+
* const client = useClient({apiVersion: '2024-11-12'})
|
|
21
20
|
* const [document, setDocument] = useState(null)
|
|
22
21
|
* useEffect(async () => {
|
|
23
22
|
* const doc = client.fetch('*[_id == "myDocumentId"]')
|
|
@@ -28,29 +27,9 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
28
27
|
* ```
|
|
29
28
|
*
|
|
30
29
|
* @public
|
|
30
|
+
* @function
|
|
31
31
|
*/
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(onStoreChange: () => void) => {
|
|
37
|
-
const client$ = getSubscribableClient(instance, options)
|
|
38
|
-
const subscription = client$.subscribe({
|
|
39
|
-
next: onStoreChange,
|
|
40
|
-
error: (error) => {
|
|
41
|
-
// @TODO: We should tackle error handling / error boundaries soon
|
|
42
|
-
// eslint-disable-next-line no-console
|
|
43
|
-
console.error('Error in useClient subscription:', error)
|
|
44
|
-
},
|
|
45
|
-
})
|
|
46
|
-
return () => subscription.unsubscribe()
|
|
47
|
-
},
|
|
48
|
-
[instance, options],
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
const getSnapshot = useCallback(() => {
|
|
52
|
-
return getClient(instance, options)
|
|
53
|
-
}, [instance, options])
|
|
54
|
-
|
|
55
|
-
return useSyncExternalStore(subscribe, getSnapshot)
|
|
56
|
-
}
|
|
32
|
+
export const useClient = createStateSourceHook({
|
|
33
|
+
getState: getClientState,
|
|
34
|
+
getResourceId: (e) => e.resourceId,
|
|
35
|
+
})
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {type ChannelInstance, type Controller, type Status} from '@sanity/comlink'
|
|
2
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {act, renderHook} from '../../../test/test-utils'
|
|
5
|
+
import {useFrameConnection} from './useFrameConnection'
|
|
6
|
+
|
|
7
|
+
vi.mock(import('@sanity/sdk'), async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal()
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
getOrCreateChannel: vi.fn(),
|
|
12
|
+
getOrCreateController: vi.fn(),
|
|
13
|
+
releaseChannel: vi.fn(),
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const {getOrCreateChannel, getOrCreateController, releaseChannel} = await import('@sanity/sdk')
|
|
18
|
+
|
|
19
|
+
interface TestControllerMessage {
|
|
20
|
+
type: 'TEST_MESSAGE'
|
|
21
|
+
data: {
|
|
22
|
+
someData: string
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TestNodeMessage {
|
|
27
|
+
type: 'NODE_MESSAGE'
|
|
28
|
+
data: {
|
|
29
|
+
someData: string
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('useFrameController', () => {
|
|
34
|
+
let channel: ChannelInstance<TestControllerMessage, TestNodeMessage>
|
|
35
|
+
let controller: Controller
|
|
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
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
channel = createMockChannel()
|
|
55
|
+
removeTargetMock = vi.fn()
|
|
56
|
+
controller = {
|
|
57
|
+
addTarget: vi.fn(() => removeTargetMock),
|
|
58
|
+
destroy: vi.fn(),
|
|
59
|
+
} as unknown as Controller
|
|
60
|
+
vi.mocked(getOrCreateChannel).mockReturnValue(channel)
|
|
61
|
+
vi.mocked(getOrCreateController).mockReturnValue(controller)
|
|
62
|
+
})
|
|
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
|
+
|
|
102
|
+
it('should register and execute message handlers', () => {
|
|
103
|
+
const mockHandler = vi.fn()
|
|
104
|
+
const mockData = {someData: 'test'}
|
|
105
|
+
renderHook(() =>
|
|
106
|
+
useFrameConnection({
|
|
107
|
+
name: 'test',
|
|
108
|
+
connectTo: 'iframe',
|
|
109
|
+
targetOrigin: '*',
|
|
110
|
+
onMessage: {
|
|
111
|
+
TEST_MESSAGE: mockHandler,
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const onCallback = vi.mocked(channel.on).mock.calls[0][1]
|
|
117
|
+
onCallback(mockData)
|
|
118
|
+
expect(mockHandler).toHaveBeenCalledWith(mockData)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should handle connecting frames and cleanup on disconnect', () => {
|
|
122
|
+
const {result} = renderHook(() =>
|
|
123
|
+
useFrameConnection({
|
|
124
|
+
name: 'test',
|
|
125
|
+
connectTo: 'iframe',
|
|
126
|
+
targetOrigin: '*',
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const mockWindow = {} as Window
|
|
131
|
+
const cleanup = result.current.connect(mockWindow)
|
|
132
|
+
|
|
133
|
+
expect(controller.addTarget).toHaveBeenCalledWith(mockWindow)
|
|
134
|
+
|
|
135
|
+
// Test cleanup
|
|
136
|
+
cleanup()
|
|
137
|
+
expect(removeTargetMock).toHaveBeenCalled()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('should send messages correctly', () => {
|
|
141
|
+
const {result} = renderHook(() =>
|
|
142
|
+
useFrameConnection<TestControllerMessage, TestNodeMessage>({
|
|
143
|
+
name: 'test',
|
|
144
|
+
connectTo: 'iframe',
|
|
145
|
+
targetOrigin: '*',
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
const mockData = {someData: 'test'}
|
|
150
|
+
result.current.sendMessage('TEST_MESSAGE', mockData)
|
|
151
|
+
|
|
152
|
+
expect(channel.post).toHaveBeenCalledWith('TEST_MESSAGE', mockData)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should cleanup on unmount', () => {
|
|
156
|
+
const {unmount} = renderHook(() =>
|
|
157
|
+
useFrameConnection({
|
|
158
|
+
name: 'test',
|
|
159
|
+
connectTo: 'iframe',
|
|
160
|
+
targetOrigin: '*',
|
|
161
|
+
}),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
unmount()
|
|
165
|
+
expect(releaseChannel).toHaveBeenCalled()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {type ChannelInstance, type Controller, type Status} from '@sanity/comlink'
|
|
2
|
+
import {
|
|
3
|
+
type FrameMessage,
|
|
4
|
+
getOrCreateChannel,
|
|
5
|
+
getOrCreateController,
|
|
6
|
+
releaseChannel,
|
|
7
|
+
type WindowMessage,
|
|
8
|
+
} from '@sanity/sdk'
|
|
9
|
+
import {useCallback, useEffect, useRef} from 'react'
|
|
10
|
+
|
|
11
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
export type FrameMessageHandler<TWindowMessage extends WindowMessage> = (
|
|
17
|
+
event: TWindowMessage['data'],
|
|
18
|
+
) => TWindowMessage['response'] | Promise<TWindowMessage['response']>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export interface UseFrameConnectionOptions<TWindowMessage extends WindowMessage> {
|
|
24
|
+
name: string
|
|
25
|
+
connectTo: string
|
|
26
|
+
targetOrigin: string
|
|
27
|
+
onMessage?: {
|
|
28
|
+
[K in TWindowMessage['type']]: (data: Extract<TWindowMessage, {type: K}>['data']) => void
|
|
29
|
+
}
|
|
30
|
+
heartbeat?: boolean
|
|
31
|
+
onStatus?: (status: Status) => void
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export interface FrameConnection<TFrameMessage extends FrameMessage> {
|
|
38
|
+
connect: (frameWindow: Window) => () => void // Return cleanup function
|
|
39
|
+
sendMessage: <T extends TFrameMessage['type']>(
|
|
40
|
+
...params: Extract<TFrameMessage, {type: T}>['data'] extends undefined
|
|
41
|
+
? [type: T]
|
|
42
|
+
: [type: T, data: Extract<TFrameMessage, {type: T}>['data']]
|
|
43
|
+
) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export function useFrameConnection<
|
|
50
|
+
TFrameMessage extends FrameMessage,
|
|
51
|
+
TWindowMessage extends WindowMessage,
|
|
52
|
+
>(options: UseFrameConnectionOptions<TWindowMessage>): FrameConnection<TFrameMessage> {
|
|
53
|
+
const {onMessage, targetOrigin, name, connectTo, heartbeat, onStatus} = options
|
|
54
|
+
const instance = useSanityInstance()
|
|
55
|
+
const controllerRef = useRef<Controller | null>(null)
|
|
56
|
+
const channelRef = useRef<ChannelInstance<TFrameMessage, TWindowMessage> | null>(null)
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const controller = getOrCreateController(instance, targetOrigin)
|
|
60
|
+
const channel = getOrCreateChannel(instance, {name, connectTo, heartbeat})
|
|
61
|
+
controllerRef.current = controller
|
|
62
|
+
channelRef.current = channel
|
|
63
|
+
|
|
64
|
+
channel.onStatus((event) => {
|
|
65
|
+
onStatus?.(event.status)
|
|
66
|
+
})
|
|
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
|
+
|
|
77
|
+
return () => {
|
|
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
|
|
83
|
+
}
|
|
84
|
+
}, [targetOrigin, name, connectTo, heartbeat, onMessage, instance, onStatus])
|
|
85
|
+
|
|
86
|
+
const connect = useCallback((frameWindow: Window) => {
|
|
87
|
+
const removeTarget = controllerRef.current?.addTarget(frameWindow)
|
|
88
|
+
return () => {
|
|
89
|
+
removeTarget?.()
|
|
90
|
+
}
|
|
91
|
+
}, [])
|
|
92
|
+
|
|
93
|
+
const sendMessage = useCallback(
|
|
94
|
+
<T extends TFrameMessage['type']>(
|
|
95
|
+
type: T,
|
|
96
|
+
data?: Extract<TFrameMessage, {type: T}>['data'],
|
|
97
|
+
) => {
|
|
98
|
+
channelRef.current?.post(type, data)
|
|
99
|
+
},
|
|
100
|
+
[],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
connect,
|
|
105
|
+
sendMessage,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -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
|
+
})
|