@sanity/sdk-react 2.11.1 → 2.13.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.
- package/dist/index.d.ts +118 -0
- package/dist/index.js +50 -32
- package/dist/index.js.map +1 -1
- package/package.json +16 -16
- package/src/_exports/sdk-react.ts +2 -0
- package/src/hooks/document/useApplyDocumentActions.test.tsx +6 -161
- package/src/hooks/document/useApplyDocumentActions.ts +3 -49
- package/src/hooks/helpers/useApplyActions.test.tsx +220 -0
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/projection/useDocumentProjection.test.tsx +41 -0
- package/src/hooks/projection/useDocumentProjection.ts +6 -0
- package/src/hooks/releases/useActiveReleases.test.tsx +11 -5
- package/src/hooks/releases/useActiveReleases.ts +12 -15
- 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 +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/sdk-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Sanity SDK React toolkit for Content OS",
|
|
6
6
|
"keywords": [
|
|
@@ -45,41 +45,41 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@sanity/client": "^7.22.0",
|
|
47
47
|
"@sanity/message-protocol": "^0.23.0",
|
|
48
|
-
"@sanity/types": "^5.
|
|
48
|
+
"@sanity/types": "^5.26.0",
|
|
49
49
|
"groq": "3.88.1-typegen-experimental.0",
|
|
50
50
|
"react-compiler-runtime": "19.1.0-rc.2",
|
|
51
|
-
"react-error-boundary": "^6.1.
|
|
51
|
+
"react-error-boundary": "^6.1.2",
|
|
52
52
|
"rxjs": "^7.8.2",
|
|
53
|
-
"@sanity/sdk": "2.
|
|
53
|
+
"@sanity/sdk": "2.13.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@sanity/browserslist-config": "^1.0.5",
|
|
57
57
|
"@sanity/comlink": "^4.0.1",
|
|
58
|
-
"@sanity/pkg-utils": "^
|
|
58
|
+
"@sanity/pkg-utils": "^9.2.3",
|
|
59
59
|
"@sanity/prettier-config": "^1.0.6",
|
|
60
60
|
"@testing-library/jest-dom": "^6.9.1",
|
|
61
61
|
"@testing-library/react": "^16.3.2",
|
|
62
|
-
"@types/node": "^24.12.
|
|
63
|
-
"@types/react": "^19.2.
|
|
62
|
+
"@types/node": "^24.12.4",
|
|
63
|
+
"@types/react": "^19.2.15",
|
|
64
64
|
"@types/react-dom": "^19.2.3",
|
|
65
|
-
"@vitejs/plugin-react": "^
|
|
66
|
-
"@vitest/coverage-v8": "4.1.
|
|
65
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
66
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
67
67
|
"babel-plugin-react-compiler": "19.1.0-rc.1",
|
|
68
68
|
"eslint": "^9.39.4",
|
|
69
|
-
"groq-js": "^1.30.
|
|
69
|
+
"groq-js": "^1.30.2",
|
|
70
70
|
"jsdom": "^29.1.1",
|
|
71
71
|
"prettier": "^3.8.3",
|
|
72
72
|
"react": "^19.2.6",
|
|
73
73
|
"react-dom": "^19.2.6",
|
|
74
74
|
"rollup-plugin-visualizer": "^5.14.0",
|
|
75
75
|
"typescript": "^5.9.3",
|
|
76
|
-
"vite": "^7.3.
|
|
77
|
-
"vitest": "^4.1.
|
|
78
|
-
"@repo/package.bundle": "3.82.0",
|
|
79
|
-
"@repo/config-test": "0.0.1",
|
|
80
|
-
"@repo/tsconfig": "0.0.1",
|
|
76
|
+
"vite": "^7.3.5",
|
|
77
|
+
"vitest": "^4.1.8",
|
|
81
78
|
"@repo/config-eslint": "0.0.0",
|
|
82
|
-
"@repo/
|
|
79
|
+
"@repo/config-test": "0.0.1",
|
|
80
|
+
"@repo/package.bundle": "3.82.0",
|
|
81
|
+
"@repo/package.config": "0.0.1",
|
|
82
|
+
"@repo/tsconfig": "0.0.1"
|
|
83
83
|
},
|
|
84
84
|
"peerDependencies": {
|
|
85
85
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -91,6 +91,8 @@ export {useProject} from '../hooks/projects/useProject'
|
|
|
91
91
|
export {type ProjectWithoutMembers, useProjects} from '../hooks/projects/useProjects'
|
|
92
92
|
export {useQuery} from '../hooks/query/useQuery'
|
|
93
93
|
export {useActiveReleases} from '../hooks/releases/useActiveReleases'
|
|
94
|
+
export {useAllReleases} from '../hooks/releases/useAllReleases'
|
|
95
|
+
export {useApplyReleaseActions} from '../hooks/releases/useApplyReleaseActions'
|
|
94
96
|
export {usePerspective} from '../hooks/releases/usePerspective'
|
|
95
97
|
export {type UserResult, useUser} from '../hooks/users/useUser'
|
|
96
98
|
export {type UsersResult, useUsers} from '../hooks/users/useUsers'
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {applyDocumentActions, createSanityInstance} from '@sanity/sdk'
|
|
2
|
-
import {renderHook as reactRenderHook} from '@testing-library/react'
|
|
3
2
|
import {describe, it} from 'vitest'
|
|
4
3
|
|
|
5
4
|
import {renderHook} from '../../../test/test-utils'
|
|
6
|
-
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
7
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
8
6
|
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
9
7
|
|
|
8
|
+
// Resource resolution, mismatch detection, and context fallback are covered
|
|
9
|
+
// by hooks/helpers/useApplyActions.test.tsx — both this hook and
|
|
10
|
+
// useApplyReleaseActions are typed wrappers over that shared implementation.
|
|
11
|
+
// These tests just verify the wrapper forwards document actions through.
|
|
12
|
+
|
|
10
13
|
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
11
14
|
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
12
15
|
return {...original, applyDocumentActions: vi.fn()}
|
|
@@ -22,7 +25,7 @@ describe('useApplyDocumentActions', () => {
|
|
|
22
25
|
vi.mocked(useSanityInstance).mockReturnValueOnce(instance)
|
|
23
26
|
})
|
|
24
27
|
|
|
25
|
-
it('
|
|
28
|
+
it('forwards a document action to applyDocumentActions with the resolved resource', () => {
|
|
26
29
|
const {result} = renderHook(() => useApplyDocumentActions())
|
|
27
30
|
result.current({
|
|
28
31
|
type: 'document.edit',
|
|
@@ -36,168 +39,10 @@ describe('useApplyDocumentActions', () => {
|
|
|
36
39
|
type: 'document.edit',
|
|
37
40
|
documentType: 'post',
|
|
38
41
|
documentId: 'abc',
|
|
39
|
-
// resource named in test-utils
|
|
40
42
|
resource: {projectId: 'test', dataset: 'test'},
|
|
41
43
|
},
|
|
42
44
|
],
|
|
43
45
|
resource: {projectId: 'test', dataset: 'test'},
|
|
44
46
|
})
|
|
45
47
|
})
|
|
46
|
-
|
|
47
|
-
it('uses the SanityInstance when resource is not provided', async () => {
|
|
48
|
-
const {result} = reactRenderHook(() => useApplyDocumentActions(), {
|
|
49
|
-
wrapper: ({children}) => (
|
|
50
|
-
<SanityInstanceContext.Provider value={instance}>{children}</SanityInstanceContext.Provider>
|
|
51
|
-
),
|
|
52
|
-
})
|
|
53
|
-
result.current(
|
|
54
|
-
{
|
|
55
|
-
type: 'document.edit',
|
|
56
|
-
documentType: 'post',
|
|
57
|
-
documentId: 'abc',
|
|
58
|
-
},
|
|
59
|
-
{},
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
63
|
-
actions: [
|
|
64
|
-
{
|
|
65
|
-
type: 'document.edit',
|
|
66
|
-
documentType: 'post',
|
|
67
|
-
documentId: 'abc',
|
|
68
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('resolves resource from projectId and dataset in action', async () => {
|
|
76
|
-
const {result} = renderHook(() => useApplyDocumentActions())
|
|
77
|
-
result.current({
|
|
78
|
-
type: 'document.edit',
|
|
79
|
-
documentType: 'post',
|
|
80
|
-
documentId: 'abc',
|
|
81
|
-
projectId: 'p',
|
|
82
|
-
dataset: 'd123',
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
86
|
-
actions: [
|
|
87
|
-
{
|
|
88
|
-
type: 'document.edit',
|
|
89
|
-
documentType: 'post',
|
|
90
|
-
documentId: 'abc',
|
|
91
|
-
resource: {projectId: 'p', dataset: 'd123'},
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
resource: {projectId: 'p', dataset: 'd123'},
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('throws when actions have mismatched project IDs', async () => {
|
|
99
|
-
const {result} = renderHook(() => useApplyDocumentActions())
|
|
100
|
-
expect(() => {
|
|
101
|
-
result.current([
|
|
102
|
-
{
|
|
103
|
-
type: 'document.edit',
|
|
104
|
-
documentType: 'post',
|
|
105
|
-
documentId: 'abc',
|
|
106
|
-
projectId: 'p123',
|
|
107
|
-
dataset: 'd',
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
type: 'document.edit',
|
|
111
|
-
documentType: 'post',
|
|
112
|
-
documentId: 'def',
|
|
113
|
-
projectId: 'p456',
|
|
114
|
-
dataset: 'd',
|
|
115
|
-
},
|
|
116
|
-
])
|
|
117
|
-
}).toThrow(/Mismatched resources found in actions/)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('throws when actions have mismatched datasets', async () => {
|
|
121
|
-
const {result} = renderHook(() => useApplyDocumentActions())
|
|
122
|
-
expect(() => {
|
|
123
|
-
result.current([
|
|
124
|
-
{
|
|
125
|
-
type: 'document.edit',
|
|
126
|
-
documentType: 'post',
|
|
127
|
-
documentId: 'abc',
|
|
128
|
-
projectId: 'p',
|
|
129
|
-
dataset: 'd1',
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
type: 'document.edit',
|
|
133
|
-
documentType: 'post',
|
|
134
|
-
documentId: 'def',
|
|
135
|
-
projectId: 'p',
|
|
136
|
-
dataset: 'd2',
|
|
137
|
-
},
|
|
138
|
-
])
|
|
139
|
-
}).toThrow(/Mismatched resources found in actions/)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('throws when actions have mismatched resources', async () => {
|
|
143
|
-
const {result} = renderHook(() => useApplyDocumentActions())
|
|
144
|
-
expect(() => {
|
|
145
|
-
result.current([
|
|
146
|
-
{
|
|
147
|
-
type: 'document.edit',
|
|
148
|
-
documentType: 'post',
|
|
149
|
-
documentId: 'abc',
|
|
150
|
-
resource: {projectId: 'p', dataset: 'd1'},
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
type: 'document.edit',
|
|
154
|
-
documentType: 'post',
|
|
155
|
-
documentId: 'def',
|
|
156
|
-
resource: {projectId: 'p', dataset: 'd2'},
|
|
157
|
-
},
|
|
158
|
-
])
|
|
159
|
-
}).toThrow(/Mismatched resources found in actions/)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('throws when mixing projectId/dataset and resource with a mismatch (projectId first)', async () => {
|
|
163
|
-
const {result} = renderHook(() => useApplyDocumentActions())
|
|
164
|
-
expect(() => {
|
|
165
|
-
result.current([
|
|
166
|
-
{
|
|
167
|
-
type: 'document.edit',
|
|
168
|
-
documentType: 'post',
|
|
169
|
-
documentId: 'abc',
|
|
170
|
-
projectId: 'p1',
|
|
171
|
-
dataset: 'd',
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
type: 'document.edit',
|
|
175
|
-
documentType: 'post',
|
|
176
|
-
documentId: 'def',
|
|
177
|
-
resource: {projectId: 'p2', dataset: 'd'},
|
|
178
|
-
},
|
|
179
|
-
])
|
|
180
|
-
}).toThrow(/Mismatched resources found in actions/)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('throws when mixing resource and projectId/dataset with a mismatch (resource first)', async () => {
|
|
184
|
-
const {result} = renderHook(() => useApplyDocumentActions())
|
|
185
|
-
expect(() => {
|
|
186
|
-
result.current([
|
|
187
|
-
{
|
|
188
|
-
type: 'document.edit',
|
|
189
|
-
documentType: 'post',
|
|
190
|
-
documentId: 'abc',
|
|
191
|
-
resource: {projectId: 'p1', dataset: 'd'},
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
type: 'document.edit',
|
|
195
|
-
documentType: 'post',
|
|
196
|
-
documentId: 'def',
|
|
197
|
-
projectId: 'p2',
|
|
198
|
-
dataset: 'd',
|
|
199
|
-
},
|
|
200
|
-
])
|
|
201
|
-
}).toThrow(/Mismatched resources found in actions/)
|
|
202
|
-
})
|
|
203
48
|
})
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import {type ActionsResult,
|
|
2
|
-
import {isDeepEqual} from '@sanity/sdk/_internal'
|
|
1
|
+
import {type ActionsResult, type DocumentAction} from '@sanity/sdk'
|
|
3
2
|
import {type SanityDocument} from 'groq'
|
|
4
|
-
import {useContext} from 'react'
|
|
5
3
|
|
|
6
4
|
import {type ResourceHandle} from '../../config/handles'
|
|
7
|
-
import {
|
|
8
|
-
import {useSanityInstance} from '../context/useSanityInstance'
|
|
9
|
-
import {
|
|
10
|
-
normalizeResourceOptions,
|
|
11
|
-
useEffectiveContextResource,
|
|
12
|
-
} from '../helpers/useNormalizedResourceOptions'
|
|
5
|
+
import {useApplyActions} from '../helpers/useApplyActions'
|
|
13
6
|
// this import is used in an `{@link useEditDocument}`
|
|
14
7
|
// eslint-disable-next-line import/consistent-type-specifier-style
|
|
15
8
|
import type {useEditDocument} from './useEditDocument'
|
|
@@ -214,44 +207,5 @@ interface UseApplyDocumentActions {
|
|
|
214
207
|
* ```
|
|
215
208
|
*/
|
|
216
209
|
export const useApplyDocumentActions: UseApplyDocumentActions = () => {
|
|
217
|
-
|
|
218
|
-
const resources = useContext(ResourcesContext)
|
|
219
|
-
const effectiveContextResource = useEffectiveContextResource()
|
|
220
|
-
|
|
221
|
-
return (actionOrActions, options) => {
|
|
222
|
-
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
|
|
223
|
-
const optionsResource = options
|
|
224
|
-
? normalizeResourceOptions(options, resources, effectiveContextResource).resource
|
|
225
|
-
: undefined
|
|
226
|
-
|
|
227
|
-
const normalizedActions = actions.map((action) =>
|
|
228
|
-
normalizeResourceOptions(action, resources, effectiveContextResource),
|
|
229
|
-
)
|
|
230
|
-
let resource
|
|
231
|
-
|
|
232
|
-
for (const action of normalizedActions) {
|
|
233
|
-
if (!resource && action.resource) resource = action.resource
|
|
234
|
-
if (!isDeepEqual(action.resource, resource)) {
|
|
235
|
-
throw new Error(
|
|
236
|
-
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
|
|
237
|
-
)
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (optionsResource && resource && !isDeepEqual(optionsResource, resource)) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`Mismatched resources found in actions. Found top-level resource "${JSON.stringify(optionsResource)}" but expected resource from action handles "${JSON.stringify(resource)}".`,
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const effectiveResource = resource ?? optionsResource ?? effectiveContextResource
|
|
248
|
-
if (!effectiveResource) {
|
|
249
|
-
throw new Error('No resource found. Provide a resource via the action handle or context.')
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return applyDocumentActions(instance, {
|
|
253
|
-
actions: normalizedActions as DocumentAction[],
|
|
254
|
-
resource: effectiveResource,
|
|
255
|
-
})
|
|
256
|
-
}
|
|
210
|
+
return useApplyActions() as ReturnType<UseApplyDocumentActions>
|
|
257
211
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {applyDocumentActions, createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {renderHook as reactRenderHook} from '@testing-library/react'
|
|
3
|
+
import {describe, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {renderHook} from '../../../test/test-utils'
|
|
6
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
7
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
8
|
+
import {useApplyActions} from './useApplyActions'
|
|
9
|
+
|
|
10
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
11
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
12
|
+
return {...original, applyDocumentActions: vi.fn()}
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
vi.mock('../context/useSanityInstance')
|
|
16
|
+
|
|
17
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
18
|
+
|
|
19
|
+
describe('useApplyActions', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.resetAllMocks()
|
|
22
|
+
vi.mocked(useSanityInstance).mockReturnValueOnce(instance)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('uses the effective context resource', async () => {
|
|
26
|
+
const {result} = renderHook(() => useApplyActions())
|
|
27
|
+
result.current({
|
|
28
|
+
type: 'document.edit',
|
|
29
|
+
documentType: 'post',
|
|
30
|
+
documentId: 'abc',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
34
|
+
actions: [
|
|
35
|
+
{
|
|
36
|
+
type: 'document.edit',
|
|
37
|
+
documentType: 'post',
|
|
38
|
+
documentId: 'abc',
|
|
39
|
+
// resource named in test-utils
|
|
40
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('uses the SanityInstance when resource is not provided', async () => {
|
|
48
|
+
const {result} = reactRenderHook(() => useApplyActions(), {
|
|
49
|
+
wrapper: ({children}) => (
|
|
50
|
+
<SanityInstanceContext.Provider value={instance}>{children}</SanityInstanceContext.Provider>
|
|
51
|
+
),
|
|
52
|
+
})
|
|
53
|
+
result.current(
|
|
54
|
+
{
|
|
55
|
+
type: 'document.edit',
|
|
56
|
+
documentType: 'post',
|
|
57
|
+
documentId: 'abc',
|
|
58
|
+
},
|
|
59
|
+
{},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
63
|
+
actions: [
|
|
64
|
+
{
|
|
65
|
+
type: 'document.edit',
|
|
66
|
+
documentType: 'post',
|
|
67
|
+
documentId: 'abc',
|
|
68
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('resolves resource from projectId and dataset on the action', async () => {
|
|
76
|
+
const {result} = renderHook(() => useApplyActions())
|
|
77
|
+
result.current({
|
|
78
|
+
type: 'document.edit',
|
|
79
|
+
documentType: 'post',
|
|
80
|
+
documentId: 'abc',
|
|
81
|
+
projectId: 'p',
|
|
82
|
+
dataset: 'd123',
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
86
|
+
actions: [
|
|
87
|
+
{
|
|
88
|
+
type: 'document.edit',
|
|
89
|
+
documentType: 'post',
|
|
90
|
+
documentId: 'abc',
|
|
91
|
+
resource: {projectId: 'p', dataset: 'd123'},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
resource: {projectId: 'p', dataset: 'd123'},
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('throws when actions have mismatched project IDs', async () => {
|
|
99
|
+
const {result} = renderHook(() => useApplyActions())
|
|
100
|
+
expect(() => {
|
|
101
|
+
result.current([
|
|
102
|
+
{
|
|
103
|
+
type: 'document.edit',
|
|
104
|
+
documentType: 'post',
|
|
105
|
+
documentId: 'abc',
|
|
106
|
+
projectId: 'p123',
|
|
107
|
+
dataset: 'd',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: 'document.edit',
|
|
111
|
+
documentType: 'post',
|
|
112
|
+
documentId: 'def',
|
|
113
|
+
projectId: 'p456',
|
|
114
|
+
dataset: 'd',
|
|
115
|
+
},
|
|
116
|
+
])
|
|
117
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('throws when actions have mismatched datasets', async () => {
|
|
121
|
+
const {result} = renderHook(() => useApplyActions())
|
|
122
|
+
expect(() => {
|
|
123
|
+
result.current([
|
|
124
|
+
{
|
|
125
|
+
type: 'document.edit',
|
|
126
|
+
documentType: 'post',
|
|
127
|
+
documentId: 'abc',
|
|
128
|
+
projectId: 'p',
|
|
129
|
+
dataset: 'd1',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: 'document.edit',
|
|
133
|
+
documentType: 'post',
|
|
134
|
+
documentId: 'def',
|
|
135
|
+
projectId: 'p',
|
|
136
|
+
dataset: 'd2',
|
|
137
|
+
},
|
|
138
|
+
])
|
|
139
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('throws when actions have mismatched resources', async () => {
|
|
143
|
+
const {result} = renderHook(() => useApplyActions())
|
|
144
|
+
expect(() => {
|
|
145
|
+
result.current([
|
|
146
|
+
{
|
|
147
|
+
type: 'document.edit',
|
|
148
|
+
documentType: 'post',
|
|
149
|
+
documentId: 'abc',
|
|
150
|
+
resource: {projectId: 'p', dataset: 'd1'},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'document.edit',
|
|
154
|
+
documentType: 'post',
|
|
155
|
+
documentId: 'def',
|
|
156
|
+
resource: {projectId: 'p', dataset: 'd2'},
|
|
157
|
+
},
|
|
158
|
+
])
|
|
159
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('throws when mixing projectId/dataset and resource with a mismatch (projectId first)', async () => {
|
|
163
|
+
const {result} = renderHook(() => useApplyActions())
|
|
164
|
+
expect(() => {
|
|
165
|
+
result.current([
|
|
166
|
+
{
|
|
167
|
+
type: 'document.edit',
|
|
168
|
+
documentType: 'post',
|
|
169
|
+
documentId: 'abc',
|
|
170
|
+
projectId: 'p1',
|
|
171
|
+
dataset: 'd',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'document.edit',
|
|
175
|
+
documentType: 'post',
|
|
176
|
+
documentId: 'def',
|
|
177
|
+
resource: {projectId: 'p2', dataset: 'd'},
|
|
178
|
+
},
|
|
179
|
+
])
|
|
180
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('throws when mixing resource and projectId/dataset with a mismatch (resource first)', async () => {
|
|
184
|
+
const {result} = renderHook(() => useApplyActions())
|
|
185
|
+
expect(() => {
|
|
186
|
+
result.current([
|
|
187
|
+
{
|
|
188
|
+
type: 'document.edit',
|
|
189
|
+
documentType: 'post',
|
|
190
|
+
documentId: 'abc',
|
|
191
|
+
resource: {projectId: 'p1', dataset: 'd'},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: 'document.edit',
|
|
195
|
+
documentType: 'post',
|
|
196
|
+
documentId: 'def',
|
|
197
|
+
projectId: 'p2',
|
|
198
|
+
dataset: 'd',
|
|
199
|
+
},
|
|
200
|
+
])
|
|
201
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('throws when a top-level options resource conflicts with an action resource', async () => {
|
|
205
|
+
const {result} = renderHook(() => useApplyActions())
|
|
206
|
+
expect(() => {
|
|
207
|
+
result.current(
|
|
208
|
+
[
|
|
209
|
+
{
|
|
210
|
+
type: 'document.edit',
|
|
211
|
+
documentType: 'post',
|
|
212
|
+
documentId: 'abc',
|
|
213
|
+
resource: {projectId: 'p', dataset: 'd1'},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
{resource: {projectId: 'p', dataset: 'd2'}},
|
|
217
|
+
)
|
|
218
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {type Action, type ActionsResult, applyDocumentActions} from '@sanity/sdk'
|
|
2
|
+
import {isDeepEqual} from '@sanity/sdk/_internal'
|
|
3
|
+
import {useContext} from 'react'
|
|
4
|
+
|
|
5
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
6
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
7
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
8
|
+
import {normalizeResourceOptions, useEffectiveContextResource} from './useNormalizedResourceOptions'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*
|
|
13
|
+
* Shared implementation behind `useApplyDocumentActions` and
|
|
14
|
+
* `useApplyReleaseActions`. Resolves the effective resource from the action
|
|
15
|
+
* handles, top-level `options`, or context (in that order), validates that
|
|
16
|
+
* everything agrees on a single resource, and forwards to
|
|
17
|
+
* `applyDocumentActions`.
|
|
18
|
+
*
|
|
19
|
+
* The two public hooks differ only in their input/output types — at runtime
|
|
20
|
+
* they're identical because core's `applyDocumentActions` accepts the full
|
|
21
|
+
* `Action` union.
|
|
22
|
+
*/
|
|
23
|
+
export function useApplyActions(): (
|
|
24
|
+
actionOrActions: Action | Action[],
|
|
25
|
+
options?: ResourceHandle,
|
|
26
|
+
) => Promise<ActionsResult> {
|
|
27
|
+
const instance = useSanityInstance()
|
|
28
|
+
const resources = useContext(ResourcesContext)
|
|
29
|
+
const effectiveContextResource = useEffectiveContextResource()
|
|
30
|
+
|
|
31
|
+
return (actionOrActions, options) => {
|
|
32
|
+
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
|
|
33
|
+
const optionsResource = options
|
|
34
|
+
? normalizeResourceOptions(options, resources, effectiveContextResource).resource
|
|
35
|
+
: undefined
|
|
36
|
+
|
|
37
|
+
const normalizedActions = actions.map((action) =>
|
|
38
|
+
normalizeResourceOptions(action, resources, effectiveContextResource),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
let resource
|
|
42
|
+
for (const action of normalizedActions) {
|
|
43
|
+
const actionResource = action['resource']
|
|
44
|
+
if (!resource && actionResource) resource = actionResource
|
|
45
|
+
if (!isDeepEqual(actionResource, resource)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(actionResource)}" but expected "${JSON.stringify(resource)}".`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (optionsResource && resource && !isDeepEqual(optionsResource, resource)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Mismatched resources found in actions. Found top-level resource "${JSON.stringify(optionsResource)}" but expected resource from action handles "${JSON.stringify(resource)}".`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const effectiveResource = resource ?? optionsResource ?? effectiveContextResource
|
|
59
|
+
if (!effectiveResource) {
|
|
60
|
+
throw new Error('No resource found. Provide a resource via the action handle or context.')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return applyDocumentActions(instance, {
|
|
64
|
+
actions: normalizedActions as Action[],
|
|
65
|
+
resource: effectiveResource,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -114,6 +114,47 @@ describe('useDocumentProjection', () => {
|
|
|
114
114
|
expect(eventsUnsubscribe).toHaveBeenCalled()
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
+
test('it re-reads the current snapshot when the element becomes visible again after an off-screen update', async () => {
|
|
118
|
+
// The store notifies subscribers only on changes *after* subscribe (it skips the
|
|
119
|
+
// value current at subscribe time), so the mocked subscribe never invokes its
|
|
120
|
+
// callback — matching real behavior. The hook must therefore re-read getCurrent()
|
|
121
|
+
// itself whenever the visibility gate (re)opens, otherwise a value that updated
|
|
122
|
+
// while the element was off-screen stays frozen on screen.
|
|
123
|
+
getCurrent.mockReturnValue({
|
|
124
|
+
data: {title: 'Initial Title', description: 'Initial Description'},
|
|
125
|
+
isPending: false,
|
|
126
|
+
})
|
|
127
|
+
const eventsUnsubscribe = vi.fn()
|
|
128
|
+
subscribe.mockImplementation(() => eventsUnsubscribe)
|
|
129
|
+
|
|
130
|
+
render(
|
|
131
|
+
<ResourceProvider fallback={<div>Loading...</div>}>
|
|
132
|
+
<TestComponent document={mockDocument} projection="{name, description}" />
|
|
133
|
+
</ResourceProvider>,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
// Become visible, then hidden — mirrors a card being scrolled out of view.
|
|
137
|
+
await act(async () => {
|
|
138
|
+
intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
|
|
139
|
+
})
|
|
140
|
+
await act(async () => {
|
|
141
|
+
intersectionObserverCallback([{isIntersecting: false} as IntersectionObserverEntry])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// The underlying document changes while the element is off-screen.
|
|
145
|
+
getCurrent.mockReturnValue({
|
|
146
|
+
data: {title: 'Updated Title', description: 'Updated Description'},
|
|
147
|
+
isPending: false,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// Becoming visible again must surface the value that changed while hidden.
|
|
151
|
+
await act(async () => {
|
|
152
|
+
intersectionObserverCallback([{isIntersecting: true} as IntersectionObserverEntry])
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(screen.getByText('Updated Title')).toBeInTheDocument()
|
|
156
|
+
})
|
|
157
|
+
|
|
117
158
|
test('it suspends and resolves data when element becomes visible', async () => {
|
|
118
159
|
// Mock the initial state to trigger suspense
|
|
119
160
|
getCurrent.mockReturnValueOnce({
|
|
@@ -235,6 +235,12 @@ export function useDocumentProjection<TData extends object>({
|
|
|
235
235
|
switchMap((isVisible) =>
|
|
236
236
|
isVisible
|
|
237
237
|
? new Observable<void>((obs) => {
|
|
238
|
+
// `stateSource.subscribe` skips the value current at subscribe time;
|
|
239
|
+
// (intentionally -- we only want new events)
|
|
240
|
+
// but in this case the store might have updated while the element was off-screen;
|
|
241
|
+
// so we fire an immediate notification here to make useSyncExternalStore
|
|
242
|
+
// re-read getCurrent() and pick up the fresh value.
|
|
243
|
+
obs.next()
|
|
238
244
|
return stateSource.subscribe(() => obs.next())
|
|
239
245
|
})
|
|
240
246
|
: EMPTY,
|