@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2
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 +103 -155
- package/dist/index.d.ts +2488 -2019
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4435 -1529
- package/dist/index.js.map +1 -1
- package/package.json +55 -59
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +30 -18
- package/src/components/SDKProvider.test.tsx +173 -32
- package/src/components/SDKProvider.tsx +87 -28
- package/src/components/SanityApp.test.tsx +68 -12
- package/src/components/SanityApp.tsx +93 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +7 -7
- package/src/components/auth/AuthBoundary.tsx +29 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +104 -61
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/CorsErrorComponent.tsx +2 -2
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +29 -27
- package/src/constants.ts +5 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
- package/src/context/ComlinkTokenRefresh.tsx +3 -5
- package/src/context/DefaultResourceContext.ts +3 -3
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/PerspectiveContext.ts +3 -3
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +93 -90
- package/src/context/ResourceProvider.tsx +62 -93
- package/src/context/SDKStudioContext.test.tsx +28 -33
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/renderSanityApp.test.tsx +151 -49
- package/src/context/renderSanityApp.tsx +12 -8
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/agent/agentActions.test.tsx +2 -2
- package/src/hooks/agent/agentActions.ts +52 -72
- package/src/hooks/agent/useAgentResourceContext.ts +1 -1
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/client/useClient.test.tsx +1 -4
- package/src/hooks/client/useClient.ts +1 -0
- package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
- package/src/hooks/comlink/useFrameConnection.ts +1 -1
- package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
- package/src/hooks/comlink/useWindowConnection.ts +2 -3
- package/src/hooks/comments/useCommentActions.test.tsx +150 -0
- package/src/hooks/comments/useCommentActions.ts +109 -0
- package/src/hooks/comments/useCommentList.ts +79 -0
- package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
- package/src/hooks/comments/useCommentThreads.ts +73 -0
- package/src/hooks/comments/useComments.test.tsx +242 -0
- package/src/hooks/comments/useComments.ts +59 -0
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +18 -1
- package/src/hooks/context/useSanityInstance.ts +6 -6
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +11 -17
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -2
- package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
- package/src/hooks/dashboard/useWindowTitle.ts +114 -0
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +33 -13
- package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
- package/src/hooks/document/useApplyDocumentActions.ts +66 -48
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.test.tsx +37 -8
- package/src/hooks/document/useDocument.ts +131 -66
- package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
- package/src/hooks/document/useDocumentEvent.ts +16 -10
- package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
- package/src/hooks/document/useDocumentPermissions.ts +71 -39
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
- package/src/hooks/document/useEditDocument.test.tsx +21 -14
- package/src/hooks/document/useEditDocument.ts +163 -17
- package/src/hooks/documents/useDocuments.test.tsx +64 -44
- package/src/hooks/documents/useDocuments.ts +31 -20
- package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
- package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +226 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
- package/src/hooks/organizations/useOrganization.test.ts +63 -0
- package/src/hooks/organizations/useOrganization.ts +34 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
- package/src/hooks/organizations/useOrganizations.test.ts +63 -0
- package/src/hooks/organizations/useOrganizations.ts +39 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +35 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +22 -20
- package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
- package/src/hooks/presence/usePresenceForDocument.ts +104 -0
- package/src/hooks/presence/useReportPresence.test.tsx +202 -0
- package/src/hooks/presence/useReportPresence.ts +170 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +12 -14
- package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
- package/src/hooks/projection/useDocumentProjection.ts +81 -2
- package/src/hooks/projects/useProject.test-d.ts +56 -0
- package/src/hooks/projects/useProject.test.tsx +120 -0
- package/src/hooks/projects/useProject.ts +42 -43
- package/src/hooks/projects/useProjects.test-d.ts +60 -0
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +17 -32
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +94 -17
- package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
- package/src/hooks/releases/useActiveReleases.ts +28 -24
- package/src/hooks/releases/useAllReleases.test.tsx +93 -0
- package/src/hooks/releases/useAllReleases.ts +62 -0
- package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
- package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
- package/src/hooks/releases/usePerspective.test.tsx +18 -10
- package/src/hooks/releases/usePerspective.ts +13 -17
- package/src/hooks/users/useUser.test.tsx +3 -9
- package/src/hooks/users/useUser.ts +4 -8
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +38 -5
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/context/useDefaultResource.test.tsx +0 -25
- package/src/hooks/context/useDefaultResource.ts +0 -30
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
- package/src/hooks/datasets/useDatasets.test.ts +0 -66
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
- package/src/hooks/projects/useProject.test.ts +0 -79
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {type SanityClient} from '@sanity/client'
|
|
2
|
+
import {getClientState, type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
3
|
+
import {firstValueFrom} from 'rxjs'
|
|
4
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {resolveOrgResources} from './resolveOrgResources'
|
|
7
|
+
|
|
8
|
+
vi.mock('@sanity/sdk', () => ({
|
|
9
|
+
getClientState: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('rxjs', () => ({
|
|
13
|
+
firstValueFrom: vi.fn(),
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const mockGetClientState = vi.mocked(getClientState)
|
|
17
|
+
const mockFirstValueFrom = vi.mocked(firstValueFrom)
|
|
18
|
+
|
|
19
|
+
const mockRequest = vi.fn()
|
|
20
|
+
const mockClient = {request: mockRequest}
|
|
21
|
+
const mockInstance = {} as SanityInstance
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.clearAllMocks()
|
|
25
|
+
mockGetClientState.mockReturnValue({
|
|
26
|
+
observable: 'mock-observable',
|
|
27
|
+
} as unknown as StateSource<SanityClient>)
|
|
28
|
+
mockFirstValueFrom.mockResolvedValue(mockClient as unknown as SanityClient)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const ORG_ID = 'org-1'
|
|
32
|
+
|
|
33
|
+
describe('resolveOrgResources', () => {
|
|
34
|
+
it('returns both mediaLibrary and canvas when both requests succeed', async () => {
|
|
35
|
+
mockRequest
|
|
36
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
37
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
38
|
+
|
|
39
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
40
|
+
|
|
41
|
+
expect(result).toEqual({
|
|
42
|
+
mediaLibrary: {mediaLibraryId: 'ml-123'},
|
|
43
|
+
canvas: {canvasId: 'canvas-456'},
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('scopes both requests to the organization', async () => {
|
|
48
|
+
mockRequest
|
|
49
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
50
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
51
|
+
|
|
52
|
+
await resolveOrgResources(mockInstance, ORG_ID)
|
|
53
|
+
|
|
54
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
55
|
+
expect.objectContaining({uri: '/media-libraries', query: {organizationId: ORG_ID}}),
|
|
56
|
+
)
|
|
57
|
+
expect(mockRequest).toHaveBeenCalledWith(
|
|
58
|
+
expect.objectContaining({uri: '/canvases', query: {organizationId: ORG_ID}}),
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('returns only mediaLibrary when canvas request fails', async () => {
|
|
63
|
+
mockRequest
|
|
64
|
+
.mockResolvedValueOnce({data: [{id: 'ml-123'}]})
|
|
65
|
+
.mockRejectedValueOnce(new Error('canvas not found'))
|
|
66
|
+
|
|
67
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
68
|
+
|
|
69
|
+
expect(result).toEqual({
|
|
70
|
+
mediaLibrary: {mediaLibraryId: 'ml-123'},
|
|
71
|
+
canvas: undefined,
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns only canvas when mediaLibrary request fails', async () => {
|
|
76
|
+
mockRequest
|
|
77
|
+
.mockRejectedValueOnce(new Error('media library not found'))
|
|
78
|
+
.mockResolvedValueOnce({data: [{id: 'canvas-456'}]})
|
|
79
|
+
|
|
80
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
mediaLibrary: undefined,
|
|
84
|
+
canvas: {canvasId: 'canvas-456'},
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('returns undefined for both when both requests fail', async () => {
|
|
89
|
+
mockRequest
|
|
90
|
+
.mockRejectedValueOnce(new Error('error 1'))
|
|
91
|
+
.mockRejectedValueOnce(new Error('error 2'))
|
|
92
|
+
|
|
93
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
94
|
+
|
|
95
|
+
expect(result).toEqual({
|
|
96
|
+
mediaLibrary: undefined,
|
|
97
|
+
canvas: undefined,
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns undefined for both when data arrays are empty', async () => {
|
|
102
|
+
mockRequest.mockResolvedValueOnce({data: []}).mockResolvedValueOnce({data: []})
|
|
103
|
+
|
|
104
|
+
const result = await resolveOrgResources(mockInstance, ORG_ID)
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual({
|
|
107
|
+
mediaLibrary: undefined,
|
|
108
|
+
canvas: undefined,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CanvasResource,
|
|
3
|
+
getClientState,
|
|
4
|
+
type MediaLibraryResource,
|
|
5
|
+
type SanityInstance,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {firstValueFrom} from 'rxjs'
|
|
8
|
+
|
|
9
|
+
const API_VERSION = 'v2026-07-09'
|
|
10
|
+
|
|
11
|
+
interface OrgResourcesApiItem {
|
|
12
|
+
id: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface OrgResourcesApiResponse {
|
|
16
|
+
data: OrgResourcesApiItem[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface OrgResources {
|
|
20
|
+
mediaLibrary?: MediaLibraryResource
|
|
21
|
+
canvas?: CanvasResource
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the id of the first resource from a settled request result. The request
|
|
26
|
+
* is already scoped to a single organization, so the first item is the one we want.
|
|
27
|
+
*/
|
|
28
|
+
function getFirstResourceId(
|
|
29
|
+
result: PromiseSettledResult<OrgResourcesApiResponse>,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
if (result.status !== 'fulfilled') return undefined
|
|
32
|
+
return result.value.data?.[0]?.id
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Fetches the media library and canvas resources for the given organization.
|
|
37
|
+
* Both requests are scoped to `organizationId` so each response contains only
|
|
38
|
+
* that org's resources. Each resource is fetched independently — a failure for
|
|
39
|
+
* one does not prevent the other from resolving.
|
|
40
|
+
*/
|
|
41
|
+
export async function resolveOrgResources(
|
|
42
|
+
instance: SanityInstance,
|
|
43
|
+
organizationId: string,
|
|
44
|
+
): Promise<OrgResources> {
|
|
45
|
+
const client = await firstValueFrom(
|
|
46
|
+
getClientState(instance, {apiVersion: API_VERSION, scope: 'global'}).observable,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const [mediaLibrariesResult, canvasesResult] = await Promise.allSettled([
|
|
50
|
+
client.request<OrgResourcesApiResponse>({
|
|
51
|
+
uri: `/media-libraries`,
|
|
52
|
+
query: {organizationId},
|
|
53
|
+
tag: 'org-resources.media-libraries',
|
|
54
|
+
}),
|
|
55
|
+
client.request<OrgResourcesApiResponse>({
|
|
56
|
+
uri: `/canvases`,
|
|
57
|
+
query: {organizationId},
|
|
58
|
+
tag: 'org-resources.canvases',
|
|
59
|
+
}),
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
const mediaLibraryId = getFirstResourceId(mediaLibrariesResult)
|
|
63
|
+
const canvasId = getFirstResourceId(canvasesResult)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
mediaLibrary: mediaLibraryId ? {mediaLibraryId} : undefined,
|
|
67
|
+
canvas: canvasId ? {canvasId} : undefined,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import {renderHook} from '@testing-library/react'
|
|
2
|
-
import {describe, expect, it} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
5
|
-
import {useResource} from './useDefaultResource'
|
|
6
|
-
|
|
7
|
-
describe('useResource', () => {
|
|
8
|
-
it('returns the resource from context', () => {
|
|
9
|
-
const resource = {projectId: 'my-project', dataset: 'my-dataset'}
|
|
10
|
-
|
|
11
|
-
const {result} = renderHook(() => useResource(), {
|
|
12
|
-
wrapper: ({children}) => (
|
|
13
|
-
<ResourceContext.Provider value={resource}>{children}</ResourceContext.Provider>
|
|
14
|
-
),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
expect(result.current).toBe(resource)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('returns undefined when no resource is in context', () => {
|
|
21
|
-
const {result} = renderHook(() => useResource())
|
|
22
|
-
|
|
23
|
-
expect(result.current).toBeUndefined()
|
|
24
|
-
})
|
|
25
|
-
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import {type DocumentResource} from '@sanity/sdk'
|
|
2
|
-
import {useContext} from 'react'
|
|
3
|
-
|
|
4
|
-
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Returns the current {@link DocumentResource} from context.
|
|
8
|
-
*
|
|
9
|
-
* @public
|
|
10
|
-
*
|
|
11
|
-
* @category Platform
|
|
12
|
-
* @returns The resource set by the nearest `ResourceProvider`, or `undefined`
|
|
13
|
-
*
|
|
14
|
-
* @remarks
|
|
15
|
-
* With the flat instance model, nested `ResourceProvider`s override the
|
|
16
|
-
* active resource via React context rather than creating child instances.
|
|
17
|
-
* Use this hook when you need the current project/dataset/resource for a
|
|
18
|
-
* subtree instead of reading from `useSanityInstance().config`.
|
|
19
|
-
*
|
|
20
|
-
* @example Get the current resource
|
|
21
|
-
* ```tsx
|
|
22
|
-
* const resource = useResource()
|
|
23
|
-
* if (resource && 'projectId' in resource) {
|
|
24
|
-
* console.log(resource.projectId, resource.dataset)
|
|
25
|
-
* }
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
export function useResource(): DocumentResource | undefined {
|
|
29
|
-
return useContext(ResourceContext)
|
|
30
|
-
}
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import {type DocumentHandle} from '@sanity/sdk'
|
|
2
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {renderHook} from '../../../test/test-utils'
|
|
5
|
-
import {useDispatchIntent} from './useDispatchIntent'
|
|
6
|
-
|
|
7
|
-
// Mock the useWindowConnection hook
|
|
8
|
-
const mockSendMessage = vi.fn()
|
|
9
|
-
vi.mock('../comlink/useWindowConnection', () => ({
|
|
10
|
-
useWindowConnection: vi.fn(() => ({
|
|
11
|
-
sendMessage: mockSendMessage,
|
|
12
|
-
})),
|
|
13
|
-
}))
|
|
14
|
-
|
|
15
|
-
describe('useDispatchIntent', () => {
|
|
16
|
-
const mockDocumentHandle: DocumentHandle = {
|
|
17
|
-
documentId: 'test-document-id',
|
|
18
|
-
documentType: 'test-document-type',
|
|
19
|
-
resource: {projectId: 'test-project-id', dataset: 'test-dataset'},
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
vi.clearAllMocks()
|
|
24
|
-
// Reset mock implementation to default behavior
|
|
25
|
-
mockSendMessage.mockImplementation(() => {})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('should return dispatchIntent function', () => {
|
|
29
|
-
const {result} = renderHook(() =>
|
|
30
|
-
useDispatchIntent({action: 'edit', documentHandle: mockDocumentHandle}),
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
expect(result.current).toEqual({
|
|
34
|
-
dispatchIntent: expect.any(Function),
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('should throw error when neither action nor intentId is provided', () => {
|
|
39
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
40
|
-
const {result} = renderHook(() => useDispatchIntent({documentHandle: mockDocumentHandle}))
|
|
41
|
-
|
|
42
|
-
expect(() => result.current.dispatchIntent()).toThrow(
|
|
43
|
-
'useDispatchIntent: Either `action` or `intentId` must be provided.',
|
|
44
|
-
)
|
|
45
|
-
consoleErrorSpy.mockRestore()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('should handle errors gracefully', () => {
|
|
49
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
50
|
-
mockSendMessage.mockImplementation(() => {
|
|
51
|
-
throw new Error('Test error')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
const {result} = renderHook(() =>
|
|
55
|
-
useDispatchIntent({action: 'edit', documentHandle: mockDocumentHandle}),
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
expect(() => result.current.dispatchIntent()).toThrow('Test error')
|
|
59
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to dispatch intent:', expect.any(Error))
|
|
60
|
-
|
|
61
|
-
consoleErrorSpy.mockRestore()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('should use memoized dispatchIntent function', () => {
|
|
65
|
-
const params = {action: 'edit' as const, documentHandle: mockDocumentHandle}
|
|
66
|
-
const {result, rerender} = renderHook(
|
|
67
|
-
({params: hookParams}: {params: typeof params}) => useDispatchIntent(hookParams),
|
|
68
|
-
{
|
|
69
|
-
initialProps: {params},
|
|
70
|
-
},
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
const firstDispatchIntent = result.current.dispatchIntent
|
|
74
|
-
|
|
75
|
-
// Rerender with the same params
|
|
76
|
-
rerender({params: {action: 'edit' as const, documentHandle: mockDocumentHandle}})
|
|
77
|
-
|
|
78
|
-
expect(result.current.dispatchIntent).toBe(firstDispatchIntent)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('should create new dispatchIntent function when documentHandle changes', () => {
|
|
82
|
-
const {result, rerender} = renderHook(
|
|
83
|
-
(params: {action: 'edit'; documentHandle: DocumentHandle}) => useDispatchIntent(params),
|
|
84
|
-
{
|
|
85
|
-
initialProps: {action: 'edit' as const, documentHandle: mockDocumentHandle},
|
|
86
|
-
},
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
const firstDispatchIntent = result.current.dispatchIntent
|
|
90
|
-
|
|
91
|
-
const newDocumentHandle: DocumentHandle = {
|
|
92
|
-
documentId: 'new-document-id',
|
|
93
|
-
documentType: 'new-document-type',
|
|
94
|
-
resource: {projectId: 'new-project-id', dataset: 'new-dataset'},
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
rerender({action: 'edit' as const, documentHandle: newDocumentHandle})
|
|
98
|
-
|
|
99
|
-
expect(result.current.dispatchIntent).not.toBe(firstDispatchIntent)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('should warn if both action and intentId are provided', () => {
|
|
103
|
-
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
104
|
-
const {result} = renderHook(() =>
|
|
105
|
-
useDispatchIntent({
|
|
106
|
-
action: 'edit' as const,
|
|
107
|
-
intentId: 'custom-intent' as never, // test runtime error when both are provided
|
|
108
|
-
documentHandle: mockDocumentHandle,
|
|
109
|
-
}),
|
|
110
|
-
)
|
|
111
|
-
result.current.dispatchIntent()
|
|
112
|
-
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
113
|
-
'useDispatchIntent: Both `action` and `intentId` were provided. Using `intentId` and ignoring `action`.',
|
|
114
|
-
)
|
|
115
|
-
consoleWarnSpy.mockRestore()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('should send intent message with action and params when both are provided', () => {
|
|
119
|
-
const intentParams = {view: 'editor', tab: 'content'}
|
|
120
|
-
const {result} = renderHook(() =>
|
|
121
|
-
useDispatchIntent({
|
|
122
|
-
action: 'edit',
|
|
123
|
-
documentHandle: mockDocumentHandle,
|
|
124
|
-
parameters: intentParams,
|
|
125
|
-
}),
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
result.current.dispatchIntent()
|
|
129
|
-
|
|
130
|
-
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
131
|
-
action: 'edit',
|
|
132
|
-
document: {
|
|
133
|
-
id: 'test-document-id',
|
|
134
|
-
type: 'test-document-type',
|
|
135
|
-
},
|
|
136
|
-
resource: {
|
|
137
|
-
id: 'test-project-id.test-dataset',
|
|
138
|
-
},
|
|
139
|
-
parameters: intentParams,
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('should send intent message with intentId and params when both are provided', () => {
|
|
144
|
-
const intentParams = {view: 'editor', tab: 'content'}
|
|
145
|
-
const {result} = renderHook(() =>
|
|
146
|
-
useDispatchIntent({
|
|
147
|
-
intentId: 'custom-intent',
|
|
148
|
-
documentHandle: mockDocumentHandle,
|
|
149
|
-
parameters: intentParams,
|
|
150
|
-
}),
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
result.current.dispatchIntent()
|
|
154
|
-
|
|
155
|
-
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
156
|
-
intentId: 'custom-intent',
|
|
157
|
-
document: {
|
|
158
|
-
id: 'test-document-id',
|
|
159
|
-
type: 'test-document-type',
|
|
160
|
-
},
|
|
161
|
-
resource: {
|
|
162
|
-
id: 'test-project-id.test-dataset',
|
|
163
|
-
},
|
|
164
|
-
parameters: intentParams,
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('should send intent message with media library resource', () => {
|
|
169
|
-
const mockMediaLibraryHandle = {
|
|
170
|
-
documentId: 'test-asset-id',
|
|
171
|
-
documentType: 'sanity.asset',
|
|
172
|
-
resourceName: 'media-library',
|
|
173
|
-
} as const
|
|
174
|
-
|
|
175
|
-
const {result} = renderHook(() =>
|
|
176
|
-
useDispatchIntent({
|
|
177
|
-
action: 'edit',
|
|
178
|
-
documentHandle: mockMediaLibraryHandle,
|
|
179
|
-
}),
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
result.current.dispatchIntent()
|
|
183
|
-
|
|
184
|
-
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
185
|
-
action: 'edit',
|
|
186
|
-
document: {
|
|
187
|
-
id: 'test-asset-id',
|
|
188
|
-
type: 'sanity.asset',
|
|
189
|
-
},
|
|
190
|
-
resource: {
|
|
191
|
-
id: 'media-library-id',
|
|
192
|
-
type: 'media-library',
|
|
193
|
-
},
|
|
194
|
-
})
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('should send intent message with canvas resource', () => {
|
|
198
|
-
const mockCanvasHandle = {
|
|
199
|
-
documentId: 'test-canvas-document-id',
|
|
200
|
-
documentType: 'sanity.canvas.document',
|
|
201
|
-
resourceName: 'canvas',
|
|
202
|
-
} as const
|
|
203
|
-
|
|
204
|
-
const {result} = renderHook(() =>
|
|
205
|
-
useDispatchIntent({
|
|
206
|
-
action: 'edit',
|
|
207
|
-
documentHandle: mockCanvasHandle,
|
|
208
|
-
}),
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
result.current.dispatchIntent()
|
|
212
|
-
|
|
213
|
-
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
214
|
-
action: 'edit',
|
|
215
|
-
document: {
|
|
216
|
-
id: 'test-canvas-document-id',
|
|
217
|
-
type: 'sanity.canvas.document',
|
|
218
|
-
},
|
|
219
|
-
resource: {
|
|
220
|
-
id: 'canvas-id',
|
|
221
|
-
type: 'canvas',
|
|
222
|
-
},
|
|
223
|
-
})
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
describe('default resource fallback', () => {
|
|
227
|
-
it('should use default resource from context when handle has no explicit targeting', () => {
|
|
228
|
-
const minimalHandle = {
|
|
229
|
-
documentId: 'test-document-id',
|
|
230
|
-
documentType: 'test-document-type',
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const {result} = renderHook(() =>
|
|
234
|
-
useDispatchIntent({
|
|
235
|
-
action: 'edit',
|
|
236
|
-
documentHandle: minimalHandle as unknown as DocumentHandle,
|
|
237
|
-
}),
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
result.current.dispatchIntent()
|
|
241
|
-
|
|
242
|
-
expect(mockSendMessage).toHaveBeenCalledWith('dashboard/v1/events/intents/dispatch-intent', {
|
|
243
|
-
action: 'edit',
|
|
244
|
-
document: {
|
|
245
|
-
id: 'test-document-id',
|
|
246
|
-
type: 'test-document-type',
|
|
247
|
-
},
|
|
248
|
-
resource: {
|
|
249
|
-
id: 'test.test',
|
|
250
|
-
},
|
|
251
|
-
})
|
|
252
|
-
})
|
|
253
|
-
})
|
|
254
|
-
})
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
2
|
-
import {type FrameMessage} from '@sanity/sdk'
|
|
3
|
-
import {useCallback} from 'react'
|
|
4
|
-
|
|
5
|
-
import {type DocumentHandle} from '../../config/handles'
|
|
6
|
-
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
7
|
-
import {useResourceIdFromDocumentHandle} from './utils/useResourceIdFromDocumentHandle'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Message type for sending intents to the dashboard
|
|
11
|
-
* @beta
|
|
12
|
-
*/
|
|
13
|
-
interface IntentMessage {
|
|
14
|
-
type: 'dashboard/v1/events/intents/dispatch-intent'
|
|
15
|
-
data: {
|
|
16
|
-
action?: 'edit'
|
|
17
|
-
intentId?: string
|
|
18
|
-
document: {
|
|
19
|
-
id: string
|
|
20
|
-
type: string
|
|
21
|
-
}
|
|
22
|
-
resource?: {
|
|
23
|
-
id: string
|
|
24
|
-
type?: 'media-library' | 'canvas'
|
|
25
|
-
}
|
|
26
|
-
parameters?: Record<string, unknown>
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Return type for the useDispatchIntent hook
|
|
32
|
-
* @beta
|
|
33
|
-
*/
|
|
34
|
-
interface DispatchIntent {
|
|
35
|
-
dispatchIntent: () => void
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Parameters for the useDispatchIntent hook
|
|
40
|
-
* @beta
|
|
41
|
-
*/
|
|
42
|
-
interface UseDispatchIntentParams {
|
|
43
|
-
action?: 'edit'
|
|
44
|
-
intentId?: string
|
|
45
|
-
documentHandle: DocumentHandle
|
|
46
|
-
parameters?: Record<string, unknown>
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @beta
|
|
51
|
-
*
|
|
52
|
-
* A hook for dispatching intent messages to the Dashboard with a document handle.
|
|
53
|
-
* This allows applications to signal their intent to pass the referenced document to other applications that have registered the ability to perform specific actions on that document.
|
|
54
|
-
*
|
|
55
|
-
* @param params - Object containing:
|
|
56
|
-
* - `action` - Action to perform (currently only 'edit' is supported). Will prompt a picker if multiple handlers are available.
|
|
57
|
-
* - `intentId` - Specific ID of the intent to dispatch. Either `action` or `intentId` is required.
|
|
58
|
-
* - `documentHandle` - The document handle containing document ID, type, and either:
|
|
59
|
-
* - `projectId` and `dataset` for traditional dataset resources, like `{documentId: '123', documentType: 'book', projectId: 'abc123', dataset: 'production'}`
|
|
60
|
-
* - `resource` for media library, canvas, or dataset resources, like `{documentId: '123', documentType: 'sanity.asset', resource: mediaLibraryResource('ml123')}` or `{documentId: '123', documentType: 'sanity.canvas.document', resource: canvasResource('canvas123')}`
|
|
61
|
-
* - `paremeters` - Optional parameters to include in the dispatch; will be passed to the resolved intent handler
|
|
62
|
-
* @returns An object containing:
|
|
63
|
-
* - `dispatchIntent` - Function to dispatch the intent message
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```tsx
|
|
67
|
-
* import {useDispatchIntent} from '@sanity/sdk-react'
|
|
68
|
-
* import {Button} from '@sanity/ui'
|
|
69
|
-
* import {Suspense} from 'react'
|
|
70
|
-
*
|
|
71
|
-
* function DispatchIntentButton({documentId, documentType, projectId, dataset}) {
|
|
72
|
-
* const {dispatchIntent} = useDispatchIntent({
|
|
73
|
-
* action: 'edit',
|
|
74
|
-
* documentHandle: {documentId, documentType, projectId, dataset},
|
|
75
|
-
* })
|
|
76
|
-
*
|
|
77
|
-
* return (
|
|
78
|
-
* <Button
|
|
79
|
-
* onClick={() => dispatchIntent()}
|
|
80
|
-
* text="Dispatch Intent"
|
|
81
|
-
* />
|
|
82
|
-
* )
|
|
83
|
-
* }
|
|
84
|
-
*
|
|
85
|
-
* // Wrap the component with Suspense since the hook may suspend
|
|
86
|
-
* function MyDocumentAction({documentId, documentType, projectId, dataset}) {
|
|
87
|
-
* return (
|
|
88
|
-
* <Suspense fallback={<Button text="Loading..." disabled />}>
|
|
89
|
-
* <DispatchIntentButton
|
|
90
|
-
* documentId={documentId}
|
|
91
|
-
* documentType={documentType}
|
|
92
|
-
* projectId={projectId}
|
|
93
|
-
* dataset={dataset}
|
|
94
|
-
* />
|
|
95
|
-
* </Suspense>
|
|
96
|
-
* )
|
|
97
|
-
* }
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
export function useDispatchIntent(params: UseDispatchIntentParams): DispatchIntent {
|
|
101
|
-
const {action, intentId, documentHandle, parameters} = params
|
|
102
|
-
const {sendMessage} = useWindowConnection<IntentMessage, FrameMessage>({
|
|
103
|
-
name: SDK_NODE_NAME,
|
|
104
|
-
connectTo: SDK_CHANNEL_NAME,
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
const resource = useResourceIdFromDocumentHandle(documentHandle)
|
|
108
|
-
|
|
109
|
-
const dispatchIntent = useCallback(() => {
|
|
110
|
-
try {
|
|
111
|
-
if (!action && !intentId) {
|
|
112
|
-
throw new Error('useDispatchIntent: Either `action` or `intentId` must be provided.')
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (action && intentId) {
|
|
116
|
-
// eslint-disable-next-line no-console -- warn if both action and intentId are provided
|
|
117
|
-
console.warn(
|
|
118
|
-
'useDispatchIntent: Both `action` and `intentId` were provided. Using `intentId` and ignoring `action`.',
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Validate that we have a resource ID (which is computed from resource/resourceName or projectId+dataset)
|
|
123
|
-
if (!resource.id) {
|
|
124
|
-
throw new Error(
|
|
125
|
-
'useDispatchIntent: Unable to determine resource. Either `resource`, `resourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const message: IntentMessage = {
|
|
130
|
-
type: 'dashboard/v1/events/intents/dispatch-intent',
|
|
131
|
-
data: {
|
|
132
|
-
...(action && !intentId ? {action} : {}),
|
|
133
|
-
...(intentId ? {intentId} : {}),
|
|
134
|
-
document: {
|
|
135
|
-
id: documentHandle.documentId,
|
|
136
|
-
type: documentHandle.documentType,
|
|
137
|
-
},
|
|
138
|
-
resource: {
|
|
139
|
-
id: resource.id,
|
|
140
|
-
...(resource.type ? {type: resource.type} : {}),
|
|
141
|
-
},
|
|
142
|
-
...(parameters && Object.keys(parameters).length > 0 ? {parameters} : {}),
|
|
143
|
-
},
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
sendMessage(message.type, message.data)
|
|
147
|
-
} catch (error) {
|
|
148
|
-
// eslint-disable-next-line no-console
|
|
149
|
-
console.error('Failed to dispatch intent:', error)
|
|
150
|
-
throw error
|
|
151
|
-
}
|
|
152
|
-
}, [action, intentId, documentHandle, parameters, sendMessage, resource.id, resource.type])
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
dispatchIntent,
|
|
156
|
-
}
|
|
157
|
-
}
|