@sanity/sdk-react 0.0.0-alpha.3 → 0.0.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -100
- package/dist/index.d.ts +2390 -2
- package/dist/index.js +1119 -2
- package/dist/index.js.map +1 -1
- package/package.json +35 -49
- package/src/_exports/index.ts +2 -10
- package/src/_exports/sdk-react.ts +73 -0
- package/src/components/SDKProvider.test.tsx +103 -0
- package/src/components/SDKProvider.tsx +52 -0
- package/src/components/SanityApp.test.tsx +244 -0
- package/src/components/SanityApp.tsx +106 -0
- package/src/components/auth/AuthBoundary.test.tsx +204 -29
- package/src/components/auth/AuthBoundary.tsx +96 -19
- package/src/components/auth/ConfigurationError.ts +22 -0
- package/src/components/auth/LoginCallback.test.tsx +22 -24
- package/src/components/auth/LoginCallback.tsx +6 -16
- package/src/components/auth/LoginError.test.tsx +11 -18
- package/src/components/auth/LoginError.tsx +43 -25
- package/src/components/utils.ts +22 -0
- package/src/context/ResourceProvider.test.tsx +157 -0
- package/src/context/ResourceProvider.tsx +111 -0
- package/src/context/SanityInstanceContext.ts +4 -0
- package/src/hooks/_synchronous-groq-js.mjs +4 -0
- package/src/hooks/auth/useAuthState.tsx +4 -5
- package/src/hooks/auth/useAuthToken.tsx +1 -1
- package/src/hooks/auth/useCurrentUser.tsx +28 -4
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +42 -0
- package/src/hooks/auth/useDashboardOrganizationId.tsx +30 -0
- package/src/hooks/auth/useHandleAuthCallback.test.tsx +16 -0
- package/src/hooks/auth/{useHandleCallback.tsx → useHandleAuthCallback.tsx} +7 -6
- package/src/hooks/auth/useLogOut.test.tsx +2 -2
- package/src/hooks/auth/useLogOut.tsx +1 -1
- package/src/hooks/auth/useLoginUrl.tsx +14 -0
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
- package/src/hooks/client/useClient.ts +13 -33
- package/src/hooks/comlink/useFrameConnection.test.tsx +167 -0
- package/src/hooks/comlink/useFrameConnection.ts +107 -0
- package/src/hooks/comlink/useManageFavorite.test.ts +368 -0
- package/src/hooks/comlink/useManageFavorite.ts +210 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.test.ts +85 -0
- package/src/hooks/comlink/useRecordDocumentHistoryEvent.ts +115 -0
- package/src/hooks/comlink/useWindowConnection.test.ts +135 -0
- package/src/hooks/comlink/useWindowConnection.ts +123 -0
- package/src/hooks/context/useSanityInstance.test.tsx +157 -15
- package/src/hooks/context/useSanityInstance.ts +68 -11
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +276 -0
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +139 -0
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +291 -0
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +101 -0
- package/src/hooks/datasets/useDatasets.test.ts +80 -0
- package/src/hooks/datasets/useDatasets.ts +52 -0
- package/src/hooks/document/useApplyDocumentActions.test.ts +20 -0
- package/src/hooks/document/useApplyDocumentActions.ts +124 -0
- package/src/hooks/document/useDocument.test.ts +118 -0
- package/src/hooks/document/useDocument.ts +212 -0
- package/src/hooks/document/useDocumentEvent.test.ts +62 -0
- package/src/hooks/document/useDocumentEvent.ts +94 -0
- package/src/hooks/document/useDocumentPermissions.test.ts +204 -0
- package/src/hooks/document/useDocumentPermissions.ts +131 -0
- package/src/hooks/document/useDocumentSyncStatus.test.ts +23 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +61 -0
- package/src/hooks/document/useEditDocument.test.ts +196 -0
- package/src/hooks/document/useEditDocument.ts +314 -0
- package/src/hooks/documents/useDocuments.test.tsx +179 -0
- package/src/hooks/documents/useDocuments.ts +300 -0
- package/src/hooks/helpers/createCallbackHook.test.tsx +2 -2
- package/src/hooks/helpers/createCallbackHook.tsx +1 -1
- package/src/hooks/helpers/createStateSourceHook.test.tsx +67 -1
- package/src/hooks/helpers/createStateSourceHook.tsx +27 -11
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +284 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +353 -0
- package/src/hooks/preview/usePreview.test.tsx +85 -17
- package/src/hooks/preview/usePreview.tsx +81 -22
- package/src/hooks/projection/useProjection.test.tsx +283 -0
- package/src/hooks/projection/useProjection.ts +232 -0
- package/src/hooks/projects/useProject.test.ts +80 -0
- package/src/hooks/projects/useProject.ts +51 -0
- package/src/hooks/projects/useProjects.test.ts +77 -0
- package/src/hooks/projects/useProjects.ts +45 -0
- package/src/hooks/query/useQuery.test.tsx +188 -0
- package/src/hooks/query/useQuery.ts +193 -0
- package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
- package/src/hooks/releases/useActiveReleases.ts +39 -0
- package/src/hooks/releases/usePerspective.test.tsx +120 -0
- package/src/hooks/releases/usePerspective.ts +49 -0
- package/src/hooks/users/useUsers.test.tsx +330 -0
- package/src/hooks/users/useUsers.ts +120 -0
- package/src/utils/getEnv.ts +21 -0
- package/src/version.ts +8 -0
- package/src/vite-env.d.ts +10 -0
- package/dist/_chunks-es/useLogOut.js +0 -44
- package/dist/_chunks-es/useLogOut.js.map +0 -1
- package/dist/assets/bundle-CcAyERuZ.css +0 -11
- package/dist/components.d.ts +0 -259
- package/dist/components.js +0 -301
- package/dist/components.js.map +0 -1
- package/dist/hooks.d.ts +0 -186
- package/dist/hooks.js +0 -81
- package/dist/hooks.js.map +0 -1
- package/src/_exports/components.ts +0 -13
- package/src/_exports/hooks.ts +0 -9
- package/src/components/DocumentGridLayout/DocumentGridLayout.stories.tsx +0 -113
- package/src/components/DocumentGridLayout/DocumentGridLayout.test.tsx +0 -42
- package/src/components/DocumentGridLayout/DocumentGridLayout.tsx +0 -21
- package/src/components/DocumentListLayout/DocumentListLayout.stories.tsx +0 -105
- package/src/components/DocumentListLayout/DocumentListLayout.test.tsx +0 -42
- package/src/components/DocumentListLayout/DocumentListLayout.tsx +0 -12
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.md +0 -49
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.stories.tsx +0 -39
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.test.tsx +0 -30
- package/src/components/DocumentPreviewLayout/DocumentPreviewLayout.tsx +0 -171
- package/src/components/Login/LoginLinks.test.tsx +0 -100
- package/src/components/Login/LoginLinks.tsx +0 -73
- package/src/components/auth/Login.test.tsx +0 -41
- package/src/components/auth/Login.tsx +0 -45
- package/src/components/auth/LoginFooter.test.tsx +0 -29
- package/src/components/auth/LoginFooter.tsx +0 -65
- package/src/components/auth/LoginLayout.test.tsx +0 -33
- package/src/components/auth/LoginLayout.tsx +0 -81
- package/src/components/context/SanityProvider.test.tsx +0 -25
- package/src/components/context/SanityProvider.tsx +0 -42
- package/src/css/css.config.js +0 -220
- package/src/css/paramour.css +0 -2347
- package/src/css/styles.css +0 -11
- package/src/hooks/auth/useHandleCallback.test.tsx +0 -16
- package/src/hooks/auth/useLoginUrls.test.tsx +0 -68
- package/src/hooks/auth/useLoginUrls.tsx +0 -51
- package/src/hooks/client/useClient.test.tsx +0 -130
- package/src/hooks/documentCollection/useDocuments.test.ts +0 -130
- package/src/hooks/documentCollection/useDocuments.ts +0 -87
|
@@ -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,30 @@
|
|
|
1
|
+
import {getDashboardOrganizationId} from '@sanity/sdk'
|
|
2
|
+
import {useMemo, useSyncExternalStore} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*
|
|
9
|
+
* A React hook that retrieves the dashboard organization ID that is currently selected in the Sanity Dashboard.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* function DashboardComponent() {
|
|
14
|
+
* const orgId = useDashboardOrganizationId()
|
|
15
|
+
*
|
|
16
|
+
* if (!orgId) return null
|
|
17
|
+
*
|
|
18
|
+
* return <div>Organization ID: {String(orgId)}</div>
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @category Dashboard
|
|
23
|
+
* @returns The dashboard organization ID (string | undefined)
|
|
24
|
+
*/
|
|
25
|
+
export function useDashboardOrganizationId(): string | undefined {
|
|
26
|
+
const instance = useSanityInstance()
|
|
27
|
+
const {subscribe, getCurrent} = useMemo(() => getDashboardOrganizationId(instance), [instance])
|
|
28
|
+
|
|
29
|
+
return useSyncExternalStore(subscribe, getCurrent)
|
|
30
|
+
}
|
|
@@ -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)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {getLoginUrlState} from '@sanity/sdk'
|
|
2
|
+
import {useMemo, useSyncExternalStore} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export function useLoginUrl(): string {
|
|
10
|
+
const instance = useSanityInstance()
|
|
11
|
+
const {subscribe, getCurrent} = useMemo(() => getLoginUrlState(instance), [instance])
|
|
12
|
+
|
|
13
|
+
return useSyncExternalStore(subscribe, getCurrent as () => string)
|
|
14
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {observeOrganizationVerificationState, type OrgVerificationResult} from '@sanity/sdk'
|
|
2
|
+
import {act, renderHook, waitFor} from '@testing-library/react'
|
|
3
|
+
import {Subject} from 'rxjs'
|
|
4
|
+
import {describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
+
import {useVerifyOrgProjects} from './useVerifyOrgProjects'
|
|
8
|
+
|
|
9
|
+
// Mock dependencies
|
|
10
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
11
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
12
|
+
return {
|
|
13
|
+
...original,
|
|
14
|
+
observeOrganizationVerificationState: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
vi.mock('../context/useSanityInstance')
|
|
18
|
+
|
|
19
|
+
describe('useVerifyOrgProjects', () => {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
const mockInstance = {config: {}} as any // Dummy instance
|
|
22
|
+
const mockObserve = vi.mocked(observeOrganizationVerificationState)
|
|
23
|
+
const mockUseInstance = vi.mocked(useSanityInstance)
|
|
24
|
+
const testProjectIds = ['proj-1']
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks()
|
|
28
|
+
mockUseInstance.mockReturnValue(mockInstance)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should return null and not observe state if disabled', () => {
|
|
32
|
+
const {result} = renderHook(() => useVerifyOrgProjects(true, testProjectIds))
|
|
33
|
+
|
|
34
|
+
expect(result.current).toBeNull()
|
|
35
|
+
expect(mockObserve).not.toHaveBeenCalled()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should return null and not observe state if projectIds is missing or empty', () => {
|
|
39
|
+
const {result: resultUndefined} = renderHook(() => useVerifyOrgProjects(false, undefined))
|
|
40
|
+
expect(resultUndefined.current).toBeNull()
|
|
41
|
+
expect(mockObserve).not.toHaveBeenCalled()
|
|
42
|
+
|
|
43
|
+
const {result: resultEmpty} = renderHook(() => useVerifyOrgProjects(false, []))
|
|
44
|
+
expect(resultEmpty.current).toBeNull()
|
|
45
|
+
expect(mockObserve).not.toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should return null initially when not disabled and projectIds provided', () => {
|
|
49
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
50
|
+
mockObserve.mockReturnValue(subject.asObservable())
|
|
51
|
+
|
|
52
|
+
const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
|
|
53
|
+
|
|
54
|
+
expect(result.current).toBeNull()
|
|
55
|
+
expect(mockObserve).toHaveBeenCalledWith(mockInstance, testProjectIds)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should return null if observable emits { error: null }', async () => {
|
|
59
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
60
|
+
mockObserve.mockReturnValue(subject.asObservable())
|
|
61
|
+
|
|
62
|
+
const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
|
|
63
|
+
|
|
64
|
+
act(() => {
|
|
65
|
+
subject.next({error: null})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(result.current).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should return error string if observable emits { error: string }', async () => {
|
|
74
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
75
|
+
const errorMessage = 'Org mismatch'
|
|
76
|
+
mockObserve.mockReturnValue(subject.asObservable())
|
|
77
|
+
|
|
78
|
+
const {result} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
|
|
79
|
+
|
|
80
|
+
act(() => {
|
|
81
|
+
subject.next({error: errorMessage})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(result.current).toBe(errorMessage)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should unsubscribe on unmount', () => {
|
|
90
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
91
|
+
const unsubscribeSpy = vi.spyOn(subject, 'unsubscribe')
|
|
92
|
+
mockObserve.mockReturnValue(subject)
|
|
93
|
+
|
|
94
|
+
const {unmount} = renderHook(() => useVerifyOrgProjects(false, testProjectIds))
|
|
95
|
+
|
|
96
|
+
expect(unsubscribeSpy).not.toHaveBeenCalled()
|
|
97
|
+
unmount()
|
|
98
|
+
// Note: RxJS handles the inner subscription cleanup when the source (Subject) completes or errors,
|
|
99
|
+
// but testing library unmount should trigger the useEffect cleanup which calls unsubscribe.
|
|
100
|
+
// However, the spy might be on the Subject itself, not the final Subscription object.
|
|
101
|
+
// Let's adjust to spy on the returned subscription directly if possible, or accept this limitation.
|
|
102
|
+
// For now, we assume the useEffect cleanup calls unsubscribe correctly.
|
|
103
|
+
// We can validate subscription logic more deeply if needed.
|
|
104
|
+
// For this test, let's check if the observable reference still has observers.
|
|
105
|
+
expect(subject.observed).toBe(false) // Check if observers are gone after unmount
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should clear the error if disabled becomes true', async () => {
|
|
109
|
+
const subject = new Subject<OrgVerificationResult>()
|
|
110
|
+
const errorMessage = 'Org mismatch'
|
|
111
|
+
mockObserve.mockReturnValue(subject.asObservable())
|
|
112
|
+
|
|
113
|
+
const {result, rerender} = renderHook(
|
|
114
|
+
({disabled, pIds}) => useVerifyOrgProjects(disabled, pIds),
|
|
115
|
+
{
|
|
116
|
+
initialProps: {disabled: false, pIds: testProjectIds},
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// Set initial error
|
|
121
|
+
act(() => {
|
|
122
|
+
subject.next({error: errorMessage})
|
|
123
|
+
})
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(result.current).toBe(errorMessage)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Disable the hook
|
|
129
|
+
rerender({disabled: true, pIds: testProjectIds})
|
|
130
|
+
|
|
131
|
+
// Error should be cleared
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(result.current).toBeNull()
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {observeOrganizationVerificationState, type OrgVerificationResult} from '@sanity/sdk'
|
|
2
|
+
import {useEffect, useState} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook that verifies the current projects belongs to the organization ID specified in the dashboard context.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
* @param disabled - When true, disables verification and skips project verification API calls
|
|
11
|
+
* @returns Error message if the project doesn't match the organization ID, or null if all match or verification isn't needed
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function OrgVerifier() {
|
|
16
|
+
* const error = useVerifyOrgProjects()
|
|
17
|
+
*
|
|
18
|
+
* if (error) {
|
|
19
|
+
* return <div className="error">{error}</div>
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* return <div>Organization projects verified!</div>
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function useVerifyOrgProjects(disabled = false, projectIds?: string[]): string | null {
|
|
27
|
+
const instance = useSanityInstance()
|
|
28
|
+
const [error, setError] = useState<string | null>(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (disabled || !projectIds || projectIds.length === 0) {
|
|
32
|
+
if (error !== null) setError(null)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const verificationObservable$ = observeOrganizationVerificationState(instance, projectIds)
|
|
37
|
+
|
|
38
|
+
const subscription = verificationObservable$.subscribe((result: OrgVerificationResult) => {
|
|
39
|
+
setError(result.error)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
subscription.unsubscribe()
|
|
44
|
+
}
|
|
45
|
+
}, [instance, disabled, error, projectIds])
|
|
46
|
+
|
|
47
|
+
return error
|
|
48
|
+
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {useCallback, useSyncExternalStore} from 'react'
|
|
1
|
+
import {getClientState} from '@sanity/sdk'
|
|
2
|
+
import {identity} from 'rxjs'
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* A React hook that provides a client that subscribes to changes in your application,
|
|
9
|
-
* such as user authentication changes.
|
|
10
8
|
*
|
|
11
9
|
* @remarks
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* This hook is intended for advanced use cases and special API calls that the React SDK
|
|
11
|
+
* does not yet provide hooks for. We welcome you to get in touch with us to let us know
|
|
12
|
+
* your use cases for this!
|
|
14
13
|
*
|
|
14
|
+
* @category Platform
|
|
15
15
|
* @returns A Sanity client
|
|
16
16
|
*
|
|
17
17
|
* @example
|
|
18
18
|
* ```tsx
|
|
19
19
|
* function MyComponent() {
|
|
20
|
-
* const client = useClient()
|
|
20
|
+
* const client = useClient({apiVersion: '2024-11-12'})
|
|
21
21
|
* const [document, setDocument] = useState(null)
|
|
22
22
|
* useEffect(async () => {
|
|
23
23
|
* const doc = client.fetch('*[_id == "myDocumentId"]')
|
|
@@ -28,29 +28,9 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
28
28
|
* ```
|
|
29
29
|
*
|
|
30
30
|
* @public
|
|
31
|
+
* @function
|
|
31
32
|
*/
|
|
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
|
-
}
|
|
33
|
+
export const useClient = createStateSourceHook({
|
|
34
|
+
getState: getClientState,
|
|
35
|
+
getConfig: identity,
|
|
36
|
+
})
|
|
@@ -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
|
+
})
|