@sanity/sdk-react 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/index.d.ts +232 -47
  2. package/dist/index.js +468 -263
  3. package/dist/index.js.map +1 -1
  4. package/package.json +8 -10
  5. package/src/_exports/sdk-react.ts +5 -0
  6. package/src/components/SDKProvider.tsx +36 -8
  7. package/src/components/SanityApp.tsx +3 -2
  8. package/src/components/auth/AuthBoundary.tsx +8 -1
  9. package/src/components/auth/DashboardAccessRequest.tsx +37 -0
  10. package/src/components/auth/LoginError.test.tsx +191 -5
  11. package/src/components/auth/LoginError.tsx +100 -56
  12. package/src/components/errors/ChunkLoadError.test.tsx +59 -0
  13. package/src/components/errors/ChunkLoadError.tsx +56 -0
  14. package/src/components/errors/chunkReloadStorage.ts +57 -0
  15. package/src/context/ResourceProvider.test.tsx +7 -1
  16. package/src/context/ResourceProvider.tsx +11 -4
  17. package/src/context/ResourcesContext.tsx +7 -0
  18. package/src/context/SDKStudioContext.ts +6 -0
  19. package/src/context/SanityInstanceProvider.test.tsx +100 -0
  20. package/src/context/SanityInstanceProvider.tsx +71 -0
  21. package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
  22. package/src/hooks/dashboard/useDispatchIntent.test.ts +8 -6
  23. package/src/hooks/dashboard/useDispatchIntent.ts +6 -6
  24. package/src/hooks/dashboard/useWindowTitle.test.ts +213 -0
  25. package/src/hooks/dashboard/useWindowTitle.ts +112 -0
  26. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
  27. package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -13
  28. package/src/hooks/document/useApplyDocumentActions.test.ts +113 -10
  29. package/src/hooks/document/useApplyDocumentActions.ts +99 -3
  30. package/src/hooks/document/useDocument.ts +22 -6
  31. package/src/hooks/document/useDocumentEvent.test.tsx +3 -3
  32. package/src/hooks/document/useDocumentEvent.ts +10 -3
  33. package/src/hooks/document/useDocumentPermissions.test.tsx +86 -2
  34. package/src/hooks/document/useDocumentPermissions.ts +22 -0
  35. package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -2
  36. package/src/hooks/document/useDocumentSyncStatus.ts +14 -5
  37. package/src/hooks/document/useEditDocument.ts +34 -8
  38. package/src/hooks/documents/useDocuments.ts +11 -6
  39. package/src/hooks/helpers/useNormalizedResourceOptions.ts +131 -0
  40. package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
  41. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +11 -8
  42. package/src/hooks/presence/usePresence.test.tsx +56 -9
  43. package/src/hooks/presence/usePresence.ts +25 -4
  44. package/src/hooks/preview/useDocumentPreview.test.tsx +84 -193
  45. package/src/hooks/preview/useDocumentPreview.tsx +40 -55
  46. package/src/hooks/projection/useDocumentProjection.ts +8 -6
  47. package/src/hooks/query/useQuery.ts +12 -9
  48. package/src/hooks/releases/useActiveReleases.ts +32 -13
  49. package/src/hooks/releases/usePerspective.ts +26 -14
  50. package/src/hooks/users/useUser.ts +2 -0
  51. package/src/hooks/users/useUsers.ts +2 -0
  52. package/src/context/SourcesContext.tsx +0 -7
  53. package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -85
@@ -0,0 +1,213 @@
1
+ import {renderHook, waitFor} from '@testing-library/react'
2
+ import {afterEach, describe, expect, it, vi} from 'vitest'
3
+
4
+ import {useWindowConnection} from '../comlink/useWindowConnection'
5
+ import {useWindowTitle} from './useWindowTitle'
6
+
7
+ vi.mock('../comlink/useWindowConnection', () => ({
8
+ useWindowConnection: vi.fn(),
9
+ }))
10
+
11
+ function createContextResponse(resource: Record<string, unknown>) {
12
+ return {context: {resource}}
13
+ }
14
+
15
+ describe('useWindowTitle', () => {
16
+ afterEach(() => {
17
+ vi.clearAllMocks()
18
+ document.title = ''
19
+ })
20
+
21
+ it('should set the document title to the manifest title', async () => {
22
+ const mockFetch = vi.fn().mockResolvedValue(
23
+ createContextResponse({
24
+ type: 'application',
25
+ title: 'sdk-movie-list',
26
+ manifest: {title: 'Movie List App'},
27
+ }),
28
+ )
29
+ vi.mocked(useWindowConnection).mockReturnValue({
30
+ fetch: mockFetch,
31
+ sendMessage: vi.fn(),
32
+ })
33
+
34
+ renderHook(() => useWindowTitle())
35
+
36
+ await waitFor(() => {
37
+ expect(document.title).toBe('Movie List App')
38
+ })
39
+ })
40
+
41
+ it('should prefer manifest title over system title', async () => {
42
+ const mockFetch = vi.fn().mockResolvedValue(
43
+ createContextResponse({
44
+ type: 'application',
45
+ title: 'sdk-movie-list',
46
+ manifest: {title: 'Movie List App'},
47
+ activeDeployment: {manifest: {title: 'Deployed Title'}},
48
+ }),
49
+ )
50
+ vi.mocked(useWindowConnection).mockReturnValue({
51
+ fetch: mockFetch,
52
+ sendMessage: vi.fn(),
53
+ })
54
+
55
+ renderHook(() => useWindowTitle())
56
+
57
+ await waitFor(() => {
58
+ expect(document.title).toBe('Movie List App')
59
+ })
60
+ })
61
+
62
+ it('should fall back to activeDeployment manifest title', async () => {
63
+ const mockFetch = vi.fn().mockResolvedValue(
64
+ createContextResponse({
65
+ type: 'application',
66
+ title: 'sdk-movie-list',
67
+ manifest: null,
68
+ activeDeployment: {manifest: {title: 'Deployed Title'}},
69
+ }),
70
+ )
71
+ vi.mocked(useWindowConnection).mockReturnValue({
72
+ fetch: mockFetch,
73
+ sendMessage: vi.fn(),
74
+ })
75
+
76
+ renderHook(() => useWindowTitle())
77
+
78
+ await waitFor(() => {
79
+ expect(document.title).toBe('Deployed Title')
80
+ })
81
+ })
82
+
83
+ it('should fall back to system title when no manifest title exists', async () => {
84
+ const mockFetch = vi.fn().mockResolvedValue(
85
+ createContextResponse({
86
+ type: 'application',
87
+ title: 'sdk-movie-list',
88
+ manifest: null,
89
+ activeDeployment: null,
90
+ }),
91
+ )
92
+ vi.mocked(useWindowConnection).mockReturnValue({
93
+ fetch: mockFetch,
94
+ sendMessage: vi.fn(),
95
+ })
96
+
97
+ renderHook(() => useWindowTitle())
98
+
99
+ await waitFor(() => {
100
+ expect(document.title).toBe('sdk-movie-list')
101
+ })
102
+ })
103
+
104
+ it('should prepend view title when provided', async () => {
105
+ const mockFetch = vi.fn().mockResolvedValue(
106
+ createContextResponse({
107
+ type: 'application',
108
+ title: 'sdk-movie-list',
109
+ manifest: {title: 'Movie List App'},
110
+ }),
111
+ )
112
+ vi.mocked(useWindowConnection).mockReturnValue({
113
+ fetch: mockFetch,
114
+ sendMessage: vi.fn(),
115
+ })
116
+
117
+ renderHook(() => useWindowTitle('Movies'))
118
+
119
+ await waitFor(() => {
120
+ expect(document.title).toBe('Movies | Movie List App')
121
+ })
122
+ })
123
+
124
+ it('should update when view title changes', async () => {
125
+ const mockFetch = vi.fn().mockResolvedValue(
126
+ createContextResponse({
127
+ type: 'application',
128
+ title: 'sdk-movie-list',
129
+ manifest: {title: 'Movie List App'},
130
+ }),
131
+ )
132
+ vi.mocked(useWindowConnection).mockReturnValue({
133
+ fetch: mockFetch,
134
+ sendMessage: vi.fn(),
135
+ })
136
+
137
+ const {rerender} = renderHook(({viewTitle}) => useWindowTitle(viewTitle), {
138
+ initialProps: {viewTitle: 'Movies'},
139
+ })
140
+
141
+ await waitFor(() => {
142
+ expect(document.title).toBe('Movies | Movie List App')
143
+ })
144
+
145
+ rerender({viewTitle: 'Movie Details'})
146
+
147
+ await waitFor(() => {
148
+ expect(document.title).toBe('Movie Details | Movie List App')
149
+ })
150
+ })
151
+
152
+ it('should restore the previous title on unmount', async () => {
153
+ document.title = 'Previous Title'
154
+
155
+ const mockFetch = vi.fn().mockResolvedValue(
156
+ createContextResponse({
157
+ type: 'application',
158
+ title: 'sdk-movie-list',
159
+ manifest: {title: 'Movie List App'},
160
+ }),
161
+ )
162
+ vi.mocked(useWindowConnection).mockReturnValue({
163
+ fetch: mockFetch,
164
+ sendMessage: vi.fn(),
165
+ })
166
+
167
+ const {unmount} = renderHook(() => useWindowTitle('Movies'))
168
+
169
+ await waitFor(() => {
170
+ expect(document.title).toBe('Movies | Movie List App')
171
+ })
172
+
173
+ unmount()
174
+
175
+ expect(document.title).toBe('Previous Title')
176
+ })
177
+
178
+ it('should handle AbortError silently', async () => {
179
+ const abortError = new Error('Aborted')
180
+ abortError.name = 'AbortError'
181
+ const mockFetch = vi.fn().mockRejectedValue(abortError)
182
+ vi.mocked(useWindowConnection).mockReturnValue({
183
+ fetch: mockFetch,
184
+ sendMessage: vi.fn(),
185
+ })
186
+
187
+ renderHook(() => useWindowTitle())
188
+
189
+ await waitFor(() => {
190
+ expect(document.title).toBe('')
191
+ })
192
+ })
193
+
194
+ it('should log non-abort fetch errors', async () => {
195
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
196
+ const mockFetch = vi.fn().mockRejectedValue(new Error('Connection failed'))
197
+ vi.mocked(useWindowConnection).mockReturnValue({
198
+ fetch: mockFetch,
199
+ sendMessage: vi.fn(),
200
+ })
201
+
202
+ renderHook(() => useWindowTitle('Movies'))
203
+
204
+ await waitFor(() => {
205
+ expect(consoleSpy).toHaveBeenCalledWith(
206
+ 'Failed to fetch app title from dashboard context:',
207
+ expect.any(Error),
208
+ )
209
+ })
210
+
211
+ consoleSpy.mockRestore()
212
+ })
213
+ })
@@ -0,0 +1,112 @@
1
+ import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
2
+ import {useEffect, useState} from 'react'
3
+
4
+ import {useWindowConnection} from '../comlink/useWindowConnection'
5
+
6
+ interface ContextResource {
7
+ type: string
8
+ title?: string
9
+ manifest?: {
10
+ title?: string
11
+ } | null
12
+ activeDeployment?: {
13
+ manifest?: {
14
+ title?: string
15
+ } | null
16
+ } | null
17
+ }
18
+
19
+ interface ContextResponse {
20
+ context: {
21
+ resource: ContextResource
22
+ }
23
+ }
24
+
25
+ function resolveAppTitle(resource: ContextResource): string | undefined {
26
+ return resource.manifest?.title ?? resource.activeDeployment?.manifest?.title ?? resource.title
27
+ }
28
+
29
+ /**
30
+ * Sets the browser's document title, automatically including the app's name
31
+ * from the manifest.
32
+ *
33
+ * This follows the same convention as Sanity Studio workspaces, where the
34
+ * workspace name is always present in the title:
35
+ *
36
+ * - With a view title: `<viewTitle> | <appTitle>`
37
+ * - Without a view title: `<appTitle>`
38
+ *
39
+ * The Sanity dashboard appends `| Sanity` to produce the final browser tab title.
40
+ *
41
+ * @param viewTitle - An optional view-specific title to prepend to the app title.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * import {useWindowTitle} from '@sanity/sdk-react'
46
+ *
47
+ * function MoviesList() {
48
+ * useWindowTitle('Movies')
49
+ * return <div>...</div>
50
+ * }
51
+ *
52
+ * // Browser tab: "Movies | My App | Sanity"
53
+ * ```
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * // Call without arguments to show just the app title
58
+ * function AppRoot() {
59
+ * useWindowTitle()
60
+ * return <Outlet />
61
+ * }
62
+ *
63
+ * // Browser tab: "My App | Sanity"
64
+ * ```
65
+ *
66
+ * @public
67
+ */
68
+ export function useWindowTitle(viewTitle?: string): void {
69
+ const [appTitle, setAppTitle] = useState<string | null>(null)
70
+
71
+ const {fetch} = useWindowConnection({
72
+ name: SDK_NODE_NAME,
73
+ connectTo: SDK_CHANNEL_NAME,
74
+ })
75
+
76
+ useEffect(() => {
77
+ if (!fetch) return
78
+
79
+ const controller = new AbortController()
80
+
81
+ async function fetchAppTitle(signal: AbortSignal) {
82
+ try {
83
+ const data = await fetch<ContextResponse>('dashboard/v1/context', undefined, {signal})
84
+ const title = resolveAppTitle(data.context.resource)
85
+ if (title) {
86
+ setAppTitle(title)
87
+ }
88
+ } catch (err: unknown) {
89
+ if (err instanceof Error && err.name === 'AbortError') return
90
+ // eslint-disable-next-line no-console
91
+ console.error('Failed to fetch app title from dashboard context:', err)
92
+ }
93
+ }
94
+
95
+ fetchAppTitle(controller.signal)
96
+
97
+ return () => {
98
+ controller.abort()
99
+ }
100
+ }, [fetch])
101
+
102
+ useEffect(() => {
103
+ if (!appTitle) return
104
+
105
+ const previous = document.title
106
+ document.title = viewTitle ? `${viewTitle} | ${appTitle}` : appTitle
107
+
108
+ return () => {
109
+ document.title = previous
110
+ }
111
+ }, [viewTitle, appTitle])
112
+ }
@@ -23,11 +23,11 @@ describe('getResourceIdFromDocumentHandle', () => {
23
23
  })
24
24
 
25
25
  describe('with DocumentHandleWithSource - media library', () => {
26
- it('should return media library ID and resourceType when media library source is provided', () => {
26
+ it('should return media library ID and resourceType when media library resource is provided', () => {
27
27
  const documentHandle = {
28
28
  documentId: 'test-asset-id',
29
29
  documentType: 'sanity.asset',
30
- sourceName: 'media-library',
30
+ resourceName: 'media-library',
31
31
  } as const
32
32
 
33
33
  const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
@@ -38,13 +38,13 @@ describe('getResourceIdFromDocumentHandle', () => {
38
38
  })
39
39
  })
40
40
 
41
- it('should prioritize source over projectId/dataset when both are provided', () => {
41
+ it('should prioritize resource over projectId/dataset when both are provided', () => {
42
42
  const documentHandle = {
43
43
  documentId: 'test-asset-id',
44
44
  documentType: 'sanity.asset',
45
45
  projectId: 'test-project-id',
46
46
  dataset: 'test-dataset',
47
- sourceName: 'media-library',
47
+ resourceName: 'media-library',
48
48
  }
49
49
 
50
50
  const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
@@ -57,11 +57,11 @@ describe('getResourceIdFromDocumentHandle', () => {
57
57
  })
58
58
 
59
59
  describe('with DocumentHandleWithSource - canvas', () => {
60
- it('should return canvas ID and resourceType when canvas source is provided', () => {
60
+ it('should return canvas ID and resourceType when canvas resource is provided', () => {
61
61
  const documentHandle = {
62
62
  documentId: 'test-canvas-document-id',
63
63
  documentType: 'sanity.canvas.document',
64
- sourceName: 'canvas',
64
+ resourceName: 'canvas',
65
65
  }
66
66
 
67
67
  const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
@@ -73,48 +73,48 @@ describe('getResourceIdFromDocumentHandle', () => {
73
73
  })
74
74
  })
75
75
 
76
- describe('with DocumentHandleWithSource - dataset source', () => {
77
- it('should return dataset resource ID when dataset source is provided', () => {
76
+ describe('with DocumentHandleWithSource - dataset resource', () => {
77
+ it('should return dataset resource ID when dataset resource is provided', () => {
78
78
  const documentHandle = {
79
79
  documentId: 'test-document-id',
80
80
  documentType: 'test-document-type',
81
- sourceName: 'dataset',
81
+ resourceName: 'dataset',
82
82
  }
83
83
 
84
84
  const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
85
85
 
86
86
  expect(result.current).toEqual({
87
- id: 'source-project-id.source-dataset',
87
+ id: 'resource-project-id.resource-dataset',
88
88
  type: undefined,
89
89
  })
90
90
  })
91
91
 
92
- it('should use dataset source over projectId/dataset when both are provided', () => {
92
+ it('should use dataset resource over projectId/dataset when both are provided', () => {
93
93
  const documentHandle = {
94
94
  documentId: 'test-document-id',
95
95
  documentType: 'test-document-type',
96
96
  projectId: 'test-project-id',
97
97
  dataset: 'test-dataset',
98
- sourceName: 'dataset',
98
+ resourceName: 'dataset',
99
99
  }
100
100
 
101
101
  const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
102
102
 
103
103
  expect(result.current).toEqual({
104
- id: 'source-project-id.source-dataset',
104
+ id: 'resource-project-id.resource-dataset',
105
105
  type: undefined,
106
106
  })
107
107
  })
108
108
  })
109
109
 
110
110
  describe('edge cases', () => {
111
- it('should handle DocumentHandleWithSource with undefined source', () => {
111
+ it('should handle DocumentHandleWithSource with undefined resource', () => {
112
112
  const documentHandle = {
113
113
  documentId: 'test-document-id',
114
114
  documentType: 'test-document-type',
115
115
  projectId: 'test-project-id',
116
116
  dataset: 'test-dataset',
117
- sourceName: undefined,
117
+ resourceName: undefined,
118
118
  }
119
119
 
120
120
  const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  type DocumentHandle,
3
- isCanvasSource,
4
- isDatasetSource,
5
- isMediaLibrarySource,
3
+ isCanvasResource,
4
+ isDatasetResource,
5
+ isMediaLibraryResource,
6
6
  } from '@sanity/sdk'
7
7
 
8
- import {useNormalizedSourceOptions} from '../../helpers/useNormalizedSourceOptions'
8
+ import {useNormalizedResourceOptions} from '../../helpers/useNormalizedResourceOptions'
9
9
 
10
10
  interface DashboardMessageResource {
11
11
  id: string
@@ -18,23 +18,23 @@ interface DashboardMessageResource {
18
18
  export function useResourceIdFromDocumentHandle(
19
19
  documentHandle: DocumentHandle,
20
20
  ): DashboardMessageResource {
21
- const options = useNormalizedSourceOptions(documentHandle)
22
- const {projectId, dataset, source} = options
21
+ const options = useNormalizedResourceOptions(documentHandle)
22
+ const {projectId, dataset, resource} = options
23
23
  let resourceId: string = ''
24
24
  let resourceType: 'media-library' | 'canvas' | undefined
25
25
  if (projectId && dataset) {
26
26
  resourceId = `${projectId}.${dataset}`
27
27
  }
28
28
 
29
- if (source) {
30
- if (isDatasetSource(source)) {
31
- resourceId = `${source.projectId}.${source.dataset}`
29
+ if (resource) {
30
+ if (isDatasetResource(resource)) {
31
+ resourceId = `${resource.projectId}.${resource.dataset}`
32
32
  resourceType = undefined
33
- } else if (isMediaLibrarySource(source)) {
34
- resourceId = source.mediaLibraryId
33
+ } else if (isMediaLibraryResource(resource)) {
34
+ resourceId = resource.mediaLibraryId
35
35
  resourceType = 'media-library'
36
- } else if (isCanvasSource(source)) {
37
- resourceId = source.canvasId
36
+ } else if (isCanvasResource(resource)) {
37
+ resourceId = resource.canvasId
38
38
  resourceType = 'canvas'
39
39
  }
40
40
  }
@@ -1,6 +1,7 @@
1
1
  import {applyDocumentActions, type SanityInstance} from '@sanity/sdk'
2
2
  import {describe, it} from 'vitest'
3
3
 
4
+ import {renderHook} from '../../../test/test-utils'
4
5
  import {useSanityInstance} from '../context/useSanityInstance'
5
6
  import {useApplyDocumentActions} from './useApplyDocumentActions'
6
7
 
@@ -31,8 +32,8 @@ describe('useApplyDocumentActions', () => {
31
32
  })
32
33
 
33
34
  it('uses the SanityInstance', async () => {
34
- const apply = useApplyDocumentActions()
35
- apply({
35
+ const {result} = renderHook(() => useApplyDocumentActions())
36
+ result.current({
36
37
  type: 'document.edit',
37
38
  documentType: 'post',
38
39
  documentId: 'abc',
@@ -50,8 +51,8 @@ describe('useApplyDocumentActions', () => {
50
51
  })
51
52
 
52
53
  it('uses SanityInstance.match when projectId is overrideen', async () => {
53
- const apply = useApplyDocumentActions()
54
- apply({
54
+ const {result} = renderHook(() => useApplyDocumentActions())
55
+ result.current({
55
56
  type: 'document.edit',
56
57
  documentType: 'post',
57
58
  documentId: 'abc',
@@ -73,8 +74,8 @@ describe('useApplyDocumentActions', () => {
73
74
  })
74
75
 
75
76
  it('uses SanityInstance when dataset is overrideen', async () => {
76
- const apply = useApplyDocumentActions()
77
- apply({
77
+ const {result} = renderHook(() => useApplyDocumentActions())
78
+ result.current({
78
79
  type: 'document.edit',
79
80
  documentType: 'post',
80
81
  documentId: 'abc',
@@ -96,8 +97,8 @@ describe('useApplyDocumentActions', () => {
96
97
  })
97
98
 
98
99
  it('uses SanityInstance.amcth when projectId and dataset is overrideen', async () => {
99
- const apply = useApplyDocumentActions()
100
- apply({
100
+ const {result} = renderHook(() => useApplyDocumentActions())
101
+ result.current({
101
102
  type: 'document.edit',
102
103
  documentType: 'post',
103
104
  documentId: 'abc',
@@ -121,9 +122,9 @@ describe('useApplyDocumentActions', () => {
121
122
  })
122
123
 
123
124
  it("throws if SanityInstance.match doesn't find anything", async () => {
124
- const apply = useApplyDocumentActions()
125
+ const {result} = renderHook(() => useApplyDocumentActions())
125
126
  expect(() => {
126
- apply({
127
+ result.current({
127
128
  type: 'document.edit',
128
129
  documentType: 'post',
129
130
  documentId: 'abc',
@@ -132,4 +133,106 @@ describe('useApplyDocumentActions', () => {
132
133
  })
133
134
  }).toThrow()
134
135
  })
136
+
137
+ it('throws when actions have mismatched project IDs', async () => {
138
+ const {result} = renderHook(() => useApplyDocumentActions())
139
+ expect(() => {
140
+ result.current([
141
+ {
142
+ type: 'document.edit',
143
+ documentType: 'post',
144
+ documentId: 'abc',
145
+ projectId: 'p123',
146
+ },
147
+ {
148
+ type: 'document.edit',
149
+ documentType: 'post',
150
+ documentId: 'def',
151
+ projectId: 'p456',
152
+ },
153
+ ])
154
+ }).toThrow(/Mismatched project IDs found in actions/)
155
+ })
156
+
157
+ it('throws when actions have mismatched datasets', async () => {
158
+ const {result} = renderHook(() => useApplyDocumentActions())
159
+ expect(() => {
160
+ result.current([
161
+ {
162
+ type: 'document.edit',
163
+ documentType: 'post',
164
+ documentId: 'abc',
165
+ projectId: 'p',
166
+ dataset: 'd1',
167
+ },
168
+ {
169
+ type: 'document.edit',
170
+ documentType: 'post',
171
+ documentId: 'def',
172
+ projectId: 'p',
173
+ dataset: 'd2',
174
+ },
175
+ ])
176
+ }).toThrow(/Mismatched datasets found in actions/)
177
+ })
178
+
179
+ it('throws when actions have mismatched resources', async () => {
180
+ const {result} = renderHook(() => useApplyDocumentActions())
181
+ expect(() => {
182
+ result.current([
183
+ {
184
+ type: 'document.edit',
185
+ documentType: 'post',
186
+ documentId: 'abc',
187
+ resource: {projectId: 'p', dataset: 'd1'},
188
+ },
189
+ {
190
+ type: 'document.edit',
191
+ documentType: 'post',
192
+ documentId: 'def',
193
+ resource: {projectId: 'p', dataset: 'd2'},
194
+ },
195
+ ])
196
+ }).toThrow(/Mismatched resources found in actions/)
197
+ })
198
+
199
+ it('throws when mixing projectId and resource (projectId first)', async () => {
200
+ const {result} = renderHook(() => useApplyDocumentActions())
201
+ expect(() => {
202
+ result.current([
203
+ {
204
+ type: 'document.edit',
205
+ documentType: 'post',
206
+ documentId: 'abc',
207
+ projectId: 'p',
208
+ },
209
+ {
210
+ type: 'document.edit',
211
+ documentType: 'post',
212
+ documentId: 'def',
213
+ resource: {projectId: 'p', dataset: 'd'},
214
+ },
215
+ ])
216
+ }).toThrow(/Mismatches between projectId\/dataset options and resource/)
217
+ })
218
+
219
+ it('throws when mixing resource and projectId (resource first)', async () => {
220
+ const {result} = renderHook(() => useApplyDocumentActions())
221
+ expect(() => {
222
+ result.current([
223
+ {
224
+ type: 'document.edit',
225
+ documentType: 'post',
226
+ documentId: 'abc',
227
+ resource: {projectId: 'p', dataset: 'd'},
228
+ },
229
+ {
230
+ type: 'document.edit',
231
+ documentType: 'post',
232
+ documentId: 'def',
233
+ projectId: 'p',
234
+ },
235
+ ])
236
+ }).toThrow(/Mismatches between projectId\/dataset options and resource/)
237
+ })
135
238
  })