@sanity/sdk-react 2.10.0 → 2.11.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 +257 -200
- package/dist/index.js +364 -253
- package/dist/index.js.map +1 -1
- package/package.json +6 -9
- package/src/_exports/index.ts +2 -0
- package/src/_exports/sdk-react.ts +4 -0
- package/src/components/SDKProvider.test.tsx +5 -12
- package/src/components/SDKProvider.tsx +26 -24
- package/src/config/handles.ts +55 -0
- package/src/constants.ts +5 -0
- package/src/context/DefaultResourceContext.ts +10 -0
- package/src/context/PerspectiveContext.ts +12 -0
- package/src/context/ResourceProvider.test.tsx +2 -2
- package/src/context/ResourceProvider.tsx +53 -49
- package/src/hooks/agent/agentActions.ts +55 -38
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +42 -111
- package/src/hooks/context/useSanityInstance.ts +28 -50
- package/src/hooks/dashboard/useDispatchIntent.test.ts +5 -1
- package/src/hooks/dashboard/useDispatchIntent.ts +3 -3
- package/src/hooks/dashboard/useManageFavorite.test.tsx +16 -12
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +1 -5
- package/src/hooks/document/{useApplyDocumentActions.test.ts → useApplyDocumentActions.test.tsx} +42 -77
- package/src/hooks/document/useApplyDocumentActions.ts +28 -62
- package/src/hooks/document/useDocument.ts +3 -5
- package/src/hooks/document/useDocumentEvent.ts +4 -3
- package/src/hooks/document/useDocumentPermissions.test.tsx +58 -150
- package/src/hooks/document/useDocumentPermissions.ts +78 -55
- package/src/hooks/document/useEditDocument.test.tsx +25 -60
- package/src/hooks/document/useEditDocument.ts +1 -1
- package/src/hooks/documents/useDocuments.ts +13 -8
- package/src/hooks/helpers/createStateSourceHook.tsx +1 -2
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +85 -47
- package/src/hooks/organizations/useOrganization.test-d.ts +53 -0
- package/src/hooks/organizations/useOrganization.test.ts +65 -0
- package/src/hooks/organizations/useOrganization.ts +40 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +55 -0
- package/src/hooks/organizations/useOrganizations.test.ts +85 -0
- package/src/hooks/organizations/useOrganizations.ts +45 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +23 -9
- package/src/hooks/presence/usePresence.ts +4 -11
- package/src/hooks/preview/useDocumentPreview.tsx +4 -7
- package/src/hooks/projection/useDocumentProjection.ts +5 -7
- package/src/hooks/projects/useProject.test-d.ts +49 -0
- package/src/hooks/projects/useProject.ts +33 -41
- package/src/hooks/projects/useProjects.test-d.ts +49 -0
- package/src/hooks/projects/useProjects.ts +17 -23
- package/src/hooks/query/useQuery.ts +1 -1
- package/src/hooks/releases/useActiveReleases.ts +6 -6
- package/src/hooks/releases/usePerspective.ts +7 -12
- package/src/hooks/users/useUser.ts +1 -1
- package/src/hooks/users/useUsers.ts +1 -1
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
applyDocumentActions,
|
|
4
|
-
type ApplyDocumentActionsOptions,
|
|
5
|
-
type DocumentAction,
|
|
6
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {type ActionsResult, applyDocumentActions, type DocumentAction} from '@sanity/sdk'
|
|
2
|
+
import {isDeepEqual} from '@sanity/sdk/_internal'
|
|
7
3
|
import {type SanityDocument} from 'groq'
|
|
8
4
|
import {useContext} from 'react'
|
|
9
5
|
|
|
6
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
10
7
|
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
11
8
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
9
|
import {
|
|
13
10
|
normalizeResourceOptions,
|
|
14
|
-
|
|
11
|
+
useEffectiveContextResource,
|
|
15
12
|
} from '../helpers/useNormalizedResourceOptions'
|
|
16
13
|
// this import is used in an `{@link useEditDocument}`
|
|
17
14
|
// eslint-disable-next-line unused-imports/no-unused-imports, import/consistent-type-specifier-style
|
|
@@ -29,7 +26,7 @@ interface UseApplyDocumentActions {
|
|
|
29
26
|
action:
|
|
30
27
|
| DocumentAction<TDocumentType, TDataset, TProjectId>
|
|
31
28
|
| DocumentAction<TDocumentType, TDataset, TProjectId>[],
|
|
32
|
-
options?:
|
|
29
|
+
options?: ResourceHandle,
|
|
33
30
|
) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
|
|
34
31
|
}
|
|
35
32
|
|
|
@@ -219,73 +216,42 @@ interface UseApplyDocumentActions {
|
|
|
219
216
|
export const useApplyDocumentActions: UseApplyDocumentActions = () => {
|
|
220
217
|
const instance = useSanityInstance()
|
|
221
218
|
const resources = useContext(ResourcesContext)
|
|
219
|
+
const effectiveContextResource = useEffectiveContextResource()
|
|
222
220
|
|
|
223
221
|
return (actionOrActions, options) => {
|
|
224
222
|
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
|
|
225
|
-
const
|
|
223
|
+
const optionsResource = options
|
|
224
|
+
? normalizeResourceOptions(options, resources, effectiveContextResource).resource
|
|
225
|
+
: undefined
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
const normalizedActions = actions.map((action) =>
|
|
228
|
+
normalizeResourceOptions(action, resources, effectiveContextResource),
|
|
229
|
+
)
|
|
229
230
|
let resource
|
|
230
|
-
for (const action of actions) {
|
|
231
|
-
if (action.projectId) {
|
|
232
|
-
if (resource) {
|
|
233
|
-
throw new Error(
|
|
234
|
-
`Mismatches between projectId/dataset options and resource in actions. Found projectId "${action.projectId}" and dataset "${action.dataset}" but expected resource "${resource}".`,
|
|
235
|
-
)
|
|
236
|
-
}
|
|
237
|
-
if (!projectId) projectId = action.projectId
|
|
238
|
-
if (action.projectId !== projectId) {
|
|
239
|
-
throw new Error(
|
|
240
|
-
`Mismatched project IDs found in actions. All actions must belong to the same project. Found "${action.projectId}" but expected "${projectId}".`,
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (action.dataset) {
|
|
245
|
-
if (!dataset) dataset = action.dataset
|
|
246
|
-
if (action.dataset !== dataset) {
|
|
247
|
-
throw new Error(
|
|
248
|
-
`Mismatched datasets found in actions. All actions must belong to the same dataset. Found "${action.dataset}" but expected "${dataset}".`,
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
231
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
throw new Error(
|
|
258
|
-
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${action.resource}" but expected "${resource}".`,
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
if (projectId || dataset) {
|
|
262
|
-
throw new Error(
|
|
263
|
-
`Mismatches between projectId/dataset options and resource in actions. Found "${action.resource}" but expected project "${projectId}" and dataset "${dataset}".`,
|
|
264
|
-
)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (projectId || dataset) {
|
|
270
|
-
const actualInstance = instance.match({projectId, dataset})
|
|
271
|
-
if (!actualInstance) {
|
|
232
|
+
for (const action of normalizedActions) {
|
|
233
|
+
if (!resource && action.resource) resource = action.resource
|
|
234
|
+
if (!isDeepEqual(action.resource, resource)) {
|
|
272
235
|
throw new Error(
|
|
273
|
-
`
|
|
274
|
-
Please ensure there is a ResourceProvider component with a matching configuration in the component hierarchy.`,
|
|
236
|
+
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
|
|
275
237
|
)
|
|
276
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
|
+
}
|
|
277
246
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
...normalizedOptions,
|
|
282
|
-
})
|
|
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.')
|
|
283
250
|
}
|
|
284
251
|
|
|
285
252
|
return applyDocumentActions(instance, {
|
|
286
|
-
actions,
|
|
287
|
-
resource,
|
|
288
|
-
...normalizedOptions,
|
|
253
|
+
actions: normalizedActions as DocumentAction[],
|
|
254
|
+
resource: effectiveResource,
|
|
289
255
|
})
|
|
290
256
|
}
|
|
291
257
|
}
|
|
@@ -2,11 +2,9 @@ import {type DocumentOptions, getDocumentState, type JsonMatch, resolveDocument}
|
|
|
2
2
|
import {type SanityDocument} from 'groq'
|
|
3
3
|
import {identity} from 'rxjs'
|
|
4
4
|
|
|
5
|
+
import {type DocumentHandle} from '../../config/handles'
|
|
5
6
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
6
|
-
import {
|
|
7
|
-
useNormalizedResourceOptions,
|
|
8
|
-
type WithResourceNameSupport,
|
|
9
|
-
} from '../helpers/useNormalizedResourceOptions'
|
|
7
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
10
8
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
11
9
|
// used in an `{@link useDocumentProjection}` and `{@link useQuery}`
|
|
12
10
|
// eslint-disable-next-line import/consistent-type-specifier-style, unused-imports/no-unused-imports
|
|
@@ -43,7 +41,7 @@ type UseDocumentOptions<
|
|
|
43
41
|
TDocumentType extends string = string,
|
|
44
42
|
TDataset extends string = string,
|
|
45
43
|
TProjectId extends string = string,
|
|
46
|
-
> =
|
|
44
|
+
> = DocumentHandle<TDocumentType, TDataset, TProjectId> & {path?: TPath}
|
|
47
45
|
|
|
48
46
|
interface UseDocument {
|
|
49
47
|
/** @internal */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type DocumentEvent, subscribeDocumentEvents} from '@sanity/sdk'
|
|
2
2
|
import {useCallback, useEffect, useInsertionEffect, useRef} from 'react'
|
|
3
3
|
|
|
4
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
4
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
6
|
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
6
7
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
@@ -11,7 +12,7 @@ import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
|
11
12
|
export interface UseDocumentEventOptions<
|
|
12
13
|
TDataset extends string = string,
|
|
13
14
|
TProjectId extends string = string,
|
|
14
|
-
> extends
|
|
15
|
+
> extends ResourceHandle<TDataset, TProjectId> {
|
|
15
16
|
onEvent: (documentEvent: DocumentEvent) => void
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -91,7 +92,7 @@ export function useDocumentEvent<
|
|
|
91
92
|
return ref.current(documentEvent)
|
|
92
93
|
}, [])
|
|
93
94
|
|
|
94
|
-
const instance = useSanityInstance(
|
|
95
|
+
const instance = useSanityInstance()
|
|
95
96
|
useEffect(() => {
|
|
96
97
|
return subscribeDocumentEvents(instance, {
|
|
97
98
|
eventHandler: stableHandler,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
|
|
2
|
-
import {
|
|
2
|
+
import {renderHook as reactRenderHook} from '@testing-library/react'
|
|
3
3
|
import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs'
|
|
4
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
|
|
5
5
|
|
|
6
|
+
import {act, renderHook, waitFor} from '../../../test/test-utils'
|
|
6
7
|
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
7
8
|
import {useDocumentPermissions} from './useDocumentPermissions'
|
|
8
9
|
|
|
@@ -24,12 +25,12 @@ vi.mock('rxjs', async (importOriginal) => {
|
|
|
24
25
|
})
|
|
25
26
|
|
|
26
27
|
describe('usePermissions', () => {
|
|
28
|
+
const mockResource = {projectId: 'project1', dataset: 'dataset1'}
|
|
27
29
|
const mockAction: DocumentAction = {
|
|
28
30
|
type: 'document.publish',
|
|
29
31
|
documentId: 'doc1',
|
|
30
32
|
documentType: 'article',
|
|
31
|
-
|
|
32
|
-
dataset: 'dataset1',
|
|
33
|
+
resource: mockResource,
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const mockPermissionAllowed: DocumentPermissionsResult = {allowed: true}
|
|
@@ -46,8 +47,8 @@ describe('usePermissions', () => {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
let permissionsSubject: BehaviorSubject<DocumentPermissionsResult | undefined>
|
|
49
|
-
let mockSubscribe:
|
|
50
|
-
let mockGetCurrent:
|
|
50
|
+
let mockSubscribe: Mock<(onStoreChanged?: () => void) => () => void>
|
|
51
|
+
let mockGetCurrent: Mock<() => DocumentPermissionsResult | undefined>
|
|
51
52
|
|
|
52
53
|
beforeEach(() => {
|
|
53
54
|
vi.clearAllMocks()
|
|
@@ -71,7 +72,7 @@ describe('usePermissions', () => {
|
|
|
71
72
|
observable:
|
|
72
73
|
permissionsSubject.asObservable() as unknown as Observable<DocumentPermissionsResult>,
|
|
73
74
|
subscribe: mockSubscribe,
|
|
74
|
-
getCurrent: mockGetCurrent,
|
|
75
|
+
getCurrent: mockGetCurrent as unknown as () => DocumentPermissionsResult,
|
|
75
76
|
})
|
|
76
77
|
})
|
|
77
78
|
|
|
@@ -85,20 +86,13 @@ describe('usePermissions', () => {
|
|
|
85
86
|
permissionsSubject.next(mockPermissionAllowed)
|
|
86
87
|
})
|
|
87
88
|
|
|
88
|
-
const {result} = renderHook(() => useDocumentPermissions(mockAction)
|
|
89
|
-
wrapper: ({children}) => (
|
|
90
|
-
<ResourceProvider
|
|
91
|
-
projectId={mockAction.projectId}
|
|
92
|
-
dataset={mockAction.dataset}
|
|
93
|
-
fallback={null}
|
|
94
|
-
>
|
|
95
|
-
{children}
|
|
96
|
-
</ResourceProvider>
|
|
97
|
-
),
|
|
98
|
-
})
|
|
89
|
+
const {result} = renderHook(() => useDocumentPermissions(mockAction))
|
|
99
90
|
|
|
100
91
|
// ResourceProvider handles the instance configuration
|
|
101
|
-
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
92
|
+
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
93
|
+
expect.any(Object),
|
|
94
|
+
expect.objectContaining({actions: [mockAction], resource: mockResource}),
|
|
95
|
+
)
|
|
102
96
|
expect(result.current).toEqual(mockPermissionAllowed)
|
|
103
97
|
})
|
|
104
98
|
|
|
@@ -108,17 +102,7 @@ describe('usePermissions', () => {
|
|
|
108
102
|
permissionsSubject.next(mockPermissionDenied)
|
|
109
103
|
})
|
|
110
104
|
|
|
111
|
-
const {result} = renderHook(() => useDocumentPermissions(mockAction)
|
|
112
|
-
wrapper: ({children}) => (
|
|
113
|
-
<ResourceProvider
|
|
114
|
-
projectId={mockAction.projectId}
|
|
115
|
-
dataset={mockAction.dataset}
|
|
116
|
-
fallback={null}
|
|
117
|
-
>
|
|
118
|
-
{children}
|
|
119
|
-
</ResourceProvider>
|
|
120
|
-
),
|
|
121
|
-
})
|
|
105
|
+
const {result} = renderHook(() => useDocumentPermissions(mockAction))
|
|
122
106
|
|
|
123
107
|
expect(result.current).toEqual(mockPermissionDenied)
|
|
124
108
|
expect(result.current.allowed).toBe(false)
|
|
@@ -129,141 +113,58 @@ describe('usePermissions', () => {
|
|
|
129
113
|
it('should accept an array of actions', () => {
|
|
130
114
|
const actions = [mockAction, {...mockAction, documentId: 'doc2'}]
|
|
131
115
|
|
|
132
|
-
renderHook(() => useDocumentPermissions(actions)
|
|
133
|
-
wrapper: ({children}) => (
|
|
134
|
-
<ResourceProvider
|
|
135
|
-
projectId={mockAction.projectId}
|
|
136
|
-
dataset={mockAction.dataset}
|
|
137
|
-
fallback={null}
|
|
138
|
-
>
|
|
139
|
-
{children}
|
|
140
|
-
</ResourceProvider>
|
|
141
|
-
),
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
expect(getPermissionsState).toHaveBeenCalledWith(expect.any(Object), {actions})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('should throw an error if actions have mismatched project IDs', () => {
|
|
148
|
-
const actions = [
|
|
149
|
-
mockAction,
|
|
150
|
-
{...mockAction, projectId: 'different-project', documentId: 'doc2'},
|
|
151
|
-
]
|
|
152
|
-
|
|
153
|
-
expect(() => {
|
|
154
|
-
renderHook(() => useDocumentPermissions(actions), {
|
|
155
|
-
wrapper: ({children}) => (
|
|
156
|
-
<ResourceProvider
|
|
157
|
-
projectId={mockAction.projectId}
|
|
158
|
-
dataset={mockAction.dataset}
|
|
159
|
-
fallback={null}
|
|
160
|
-
>
|
|
161
|
-
{children}
|
|
162
|
-
</ResourceProvider>
|
|
163
|
-
),
|
|
164
|
-
})
|
|
165
|
-
}).toThrow(/Mismatched project IDs found in actions/)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('should throw an error if actions have mismatched datasets', () => {
|
|
169
|
-
const actions = [mockAction, {...mockAction, dataset: 'different-dataset', documentId: 'doc2'}]
|
|
116
|
+
renderHook(() => useDocumentPermissions(actions))
|
|
170
117
|
|
|
171
|
-
expect((
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
projectId={mockAction.projectId}
|
|
176
|
-
dataset={mockAction.dataset}
|
|
177
|
-
fallback={null}
|
|
178
|
-
>
|
|
179
|
-
{children}
|
|
180
|
-
</ResourceProvider>
|
|
181
|
-
),
|
|
182
|
-
})
|
|
183
|
-
}).toThrow(/Mismatched datasets found in actions/)
|
|
118
|
+
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
119
|
+
expect.any(Object),
|
|
120
|
+
expect.objectContaining({actions, resource: mockResource}),
|
|
121
|
+
)
|
|
184
122
|
})
|
|
185
123
|
|
|
186
124
|
it('should throw an error if actions have mismatched resources', () => {
|
|
187
125
|
const actions = [
|
|
126
|
+
mockAction,
|
|
188
127
|
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
documentType: 'article',
|
|
192
|
-
resource: {projectId: 'p1', dataset: 'd1'},
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
type: 'document.publish' as const,
|
|
128
|
+
...mockAction,
|
|
129
|
+
resource: {projectId: 'different-project', dataset: 'dataset1'},
|
|
196
130
|
documentId: 'doc2',
|
|
197
|
-
documentType: 'article',
|
|
198
|
-
resource: {projectId: 'p2', dataset: 'd2'},
|
|
199
131
|
},
|
|
200
132
|
]
|
|
201
133
|
|
|
202
134
|
expect(() => {
|
|
203
|
-
renderHook(() => useDocumentPermissions(actions)
|
|
204
|
-
wrapper: ({children}) => (
|
|
205
|
-
<ResourceProvider
|
|
206
|
-
projectId={mockAction.projectId}
|
|
207
|
-
dataset={mockAction.dataset}
|
|
208
|
-
fallback={null}
|
|
209
|
-
>
|
|
210
|
-
{children}
|
|
211
|
-
</ResourceProvider>
|
|
212
|
-
),
|
|
213
|
-
})
|
|
135
|
+
renderHook(() => useDocumentPermissions(actions))
|
|
214
136
|
}).toThrow(/Mismatched resources found in actions/)
|
|
215
137
|
})
|
|
216
138
|
|
|
217
|
-
it('should throw an error
|
|
139
|
+
it('should throw an error if actions have mismatched datasets', () => {
|
|
218
140
|
const actions = [
|
|
219
141
|
mockAction,
|
|
220
142
|
{
|
|
221
|
-
|
|
143
|
+
...mockAction,
|
|
144
|
+
resource: {projectId: 'project1', dataset: 'different-dataset'},
|
|
222
145
|
documentId: 'doc2',
|
|
223
|
-
documentType: 'article',
|
|
224
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
225
146
|
},
|
|
226
147
|
]
|
|
227
148
|
|
|
228
149
|
expect(() => {
|
|
229
|
-
renderHook(() => useDocumentPermissions(actions)
|
|
230
|
-
|
|
231
|
-
<ResourceProvider
|
|
232
|
-
projectId={mockAction.projectId}
|
|
233
|
-
dataset={mockAction.dataset}
|
|
234
|
-
fallback={null}
|
|
235
|
-
>
|
|
236
|
-
{children}
|
|
237
|
-
</ResourceProvider>
|
|
238
|
-
),
|
|
239
|
-
})
|
|
240
|
-
}).toThrow(/Mismatches between projectId\/dataset options and resource/)
|
|
150
|
+
renderHook(() => useDocumentPermissions(actions))
|
|
151
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
241
152
|
})
|
|
242
153
|
|
|
243
|
-
it('should throw an error when mixing
|
|
154
|
+
it('should throw an error when mixing different resources', () => {
|
|
244
155
|
const actions = [
|
|
156
|
+
mockAction,
|
|
245
157
|
{
|
|
246
158
|
type: 'document.publish' as const,
|
|
247
|
-
documentId: '
|
|
159
|
+
documentId: 'doc2',
|
|
248
160
|
documentType: 'article',
|
|
249
161
|
resource: {projectId: 'p', dataset: 'd'},
|
|
250
162
|
},
|
|
251
|
-
mockAction,
|
|
252
163
|
]
|
|
253
164
|
|
|
254
165
|
expect(() => {
|
|
255
|
-
renderHook(() => useDocumentPermissions(actions)
|
|
256
|
-
|
|
257
|
-
<ResourceProvider
|
|
258
|
-
projectId={mockAction.projectId}
|
|
259
|
-
dataset={mockAction.dataset}
|
|
260
|
-
fallback={null}
|
|
261
|
-
>
|
|
262
|
-
{children}
|
|
263
|
-
</ResourceProvider>
|
|
264
|
-
),
|
|
265
|
-
})
|
|
266
|
-
}).toThrow(/Mismatches between projectId\/dataset options and resource/)
|
|
166
|
+
renderHook(() => useDocumentPermissions(actions))
|
|
167
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
267
168
|
})
|
|
268
169
|
|
|
269
170
|
it('should wait for permissions to be ready before rendering', async () => {
|
|
@@ -277,7 +178,7 @@ describe('usePermissions', () => {
|
|
|
277
178
|
vi.mocked(firstValueFrom).mockReturnValueOnce(mockPromise)
|
|
278
179
|
|
|
279
180
|
// This should throw the promise and suspend
|
|
280
|
-
const {result} =
|
|
181
|
+
const {result} = reactRenderHook(
|
|
281
182
|
() => {
|
|
282
183
|
try {
|
|
283
184
|
return useDocumentPermissions(mockAction)
|
|
@@ -290,11 +191,7 @@ describe('usePermissions', () => {
|
|
|
290
191
|
},
|
|
291
192
|
{
|
|
292
193
|
wrapper: ({children}) => (
|
|
293
|
-
<ResourceProvider
|
|
294
|
-
projectId={mockAction.projectId}
|
|
295
|
-
dataset={mockAction.dataset}
|
|
296
|
-
fallback={null}
|
|
297
|
-
>
|
|
194
|
+
<ResourceProvider resource={mockResource} fallback={null}>
|
|
298
195
|
{children}
|
|
299
196
|
</ResourceProvider>
|
|
300
197
|
),
|
|
@@ -310,27 +207,38 @@ describe('usePermissions', () => {
|
|
|
310
207
|
|
|
311
208
|
// Now it should render properly
|
|
312
209
|
await waitFor(() => {
|
|
313
|
-
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
210
|
+
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
211
|
+
expect.any(Object),
|
|
212
|
+
expect.objectContaining({actions: [mockAction], resource: mockResource}),
|
|
213
|
+
)
|
|
314
214
|
})
|
|
315
215
|
})
|
|
316
216
|
|
|
217
|
+
it('throws when no resource is found from action or context', () => {
|
|
218
|
+
// Provide SanityInstance via ResourceProvider but no resource, so contextResource is undefined
|
|
219
|
+
expect(() => {
|
|
220
|
+
reactRenderHook(
|
|
221
|
+
() =>
|
|
222
|
+
useDocumentPermissions({
|
|
223
|
+
type: 'document.publish',
|
|
224
|
+
documentId: 'doc1',
|
|
225
|
+
documentType: 'article',
|
|
226
|
+
// no resource
|
|
227
|
+
} as never),
|
|
228
|
+
{
|
|
229
|
+
wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
232
|
+
}).toThrow(/No resource found/)
|
|
233
|
+
})
|
|
234
|
+
|
|
317
235
|
it('should react to permission state changes', async () => {
|
|
318
236
|
// Start with permission allowed
|
|
319
237
|
act(() => {
|
|
320
238
|
permissionsSubject.next(mockPermissionAllowed)
|
|
321
239
|
})
|
|
322
240
|
|
|
323
|
-
const {result, rerender} = renderHook(() => useDocumentPermissions(mockAction)
|
|
324
|
-
wrapper: ({children}) => (
|
|
325
|
-
<ResourceProvider
|
|
326
|
-
projectId={mockAction.projectId}
|
|
327
|
-
dataset={mockAction.dataset}
|
|
328
|
-
fallback={null}
|
|
329
|
-
>
|
|
330
|
-
{children}
|
|
331
|
-
</ResourceProvider>
|
|
332
|
-
),
|
|
333
|
-
})
|
|
241
|
+
const {result, rerender} = renderHook(() => useDocumentPermissions(mockAction))
|
|
334
242
|
|
|
335
243
|
expect(result.current).toEqual(mockPermissionAllowed)
|
|
336
244
|
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
|
|
2
|
-
import {
|
|
2
|
+
import {isDeepEqual} from '@sanity/sdk/_internal'
|
|
3
|
+
import {useCallback, useContext, useMemo, useSyncExternalStore} from 'react'
|
|
3
4
|
import {filter, firstValueFrom} from 'rxjs'
|
|
4
5
|
|
|
6
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
5
7
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
8
|
+
import {
|
|
9
|
+
normalizeResourceOptions,
|
|
10
|
+
useEffectiveContextResource,
|
|
11
|
+
type WithResourceNameSupport,
|
|
12
|
+
} from '../helpers/useNormalizedResourceOptions'
|
|
6
13
|
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
7
14
|
|
|
15
|
+
const noopSubscribe = () => () => {}
|
|
16
|
+
const returnUndefined = () => undefined
|
|
17
|
+
|
|
8
18
|
/**
|
|
9
19
|
*
|
|
10
20
|
* @public
|
|
@@ -83,74 +93,87 @@ import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
|
83
93
|
* ```
|
|
84
94
|
*/
|
|
85
95
|
export function useDocumentPermissions(
|
|
86
|
-
actionOrActions:
|
|
96
|
+
actionOrActions:
|
|
97
|
+
| WithResourceNameSupport<DocumentAction>
|
|
98
|
+
| WithResourceNameSupport<DocumentAction>[],
|
|
87
99
|
): DocumentPermissionsResult {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
92
|
-
// if actions is an array, we need to check that all actions belong to the same project and dataset
|
|
93
|
-
let projectId
|
|
94
|
-
let dataset
|
|
95
|
-
let resource
|
|
100
|
+
const instance = useSanityInstance()
|
|
101
|
+
trackHookUsage(instance, 'useDocumentPermissions')
|
|
102
|
+
const effectiveContextResource = useEffectiveContextResource()
|
|
103
|
+
const resources = useContext(ResourcesContext)
|
|
96
104
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
const {
|
|
106
|
+
actions: normalizedActions,
|
|
107
|
+
resource: actionResource,
|
|
108
|
+
error: validationError,
|
|
109
|
+
} = useMemo(() => {
|
|
110
|
+
const normalized = Array.isArray(actionOrActions)
|
|
111
|
+
? actionOrActions.map((action) =>
|
|
112
|
+
normalizeResourceOptions(action, resources, effectiveContextResource),
|
|
102
113
|
)
|
|
103
|
-
|
|
104
|
-
if (!projectId) projectId = action.projectId
|
|
105
|
-
if (action.projectId !== projectId) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`Mismatched project IDs found in actions. All actions must belong to the same project. Found "${action.projectId}" but expected "${projectId}".`,
|
|
108
|
-
)
|
|
109
|
-
}
|
|
114
|
+
: [normalizeResourceOptions(actionOrActions, resources, effectiveContextResource)]
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
let resource
|
|
117
|
+
for (const action of normalized) {
|
|
118
|
+
if (action.resource) {
|
|
119
|
+
if (!resource) resource = action.resource
|
|
120
|
+
if (!isDeepEqual(action.resource, resource)) {
|
|
121
|
+
return {
|
|
122
|
+
actions: normalized,
|
|
123
|
+
resource,
|
|
124
|
+
error: new Error(
|
|
125
|
+
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
|
|
126
|
+
),
|
|
127
|
+
}
|
|
117
128
|
}
|
|
118
129
|
}
|
|
119
130
|
}
|
|
131
|
+
return {actions: normalized, resource, error: undefined}
|
|
132
|
+
}, [actionOrActions, resources, effectiveContextResource])
|
|
120
133
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
const effectiveResource = actionResource ?? effectiveContextResource
|
|
135
|
+
|
|
136
|
+
// Keep hooks unconditional — validation errors and missing-resource errors are
|
|
137
|
+
// thrown after all hooks so that the hook call count stays stable across renders.
|
|
138
|
+
const permissionsOptions = useMemo(
|
|
139
|
+
() =>
|
|
140
|
+
effectiveResource
|
|
141
|
+
? {
|
|
142
|
+
resource: effectiveResource,
|
|
143
|
+
// `Omit<>` on `DocumentAction` loses the discriminant; runtime values are still actions.
|
|
144
|
+
actions: normalizedActions as DocumentAction[],
|
|
145
|
+
}
|
|
146
|
+
: undefined,
|
|
147
|
+
[effectiveResource, normalizedActions],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const stateSource = useMemo(
|
|
151
|
+
() => (permissionsOptions ? getPermissionsState(instance, permissionsOptions) : undefined),
|
|
152
|
+
[permissionsOptions, instance],
|
|
153
|
+
)
|
|
135
154
|
|
|
136
|
-
const instance = useSanityInstance({projectId, dataset})
|
|
137
|
-
trackHookUsage(instance, 'useDocumentPermissions')
|
|
138
155
|
const isDocumentReady = useCallback(
|
|
139
|
-
() =>
|
|
140
|
-
[
|
|
156
|
+
() => stateSource !== undefined && stateSource.getCurrent() !== undefined,
|
|
157
|
+
[stateSource],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const result = useSyncExternalStore(
|
|
161
|
+
stateSource?.subscribe ?? noopSubscribe,
|
|
162
|
+
stateSource?.getCurrent ?? returnUndefined,
|
|
141
163
|
)
|
|
164
|
+
|
|
165
|
+
// All hooks have been called — safe to throw now.
|
|
166
|
+
if (validationError) throw validationError
|
|
167
|
+
if (!effectiveResource) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
'No resource found. Provide a resource via the action handle or wrap with a resource context.',
|
|
170
|
+
)
|
|
171
|
+
}
|
|
142
172
|
if (!isDocumentReady()) {
|
|
143
173
|
throw firstValueFrom(
|
|
144
|
-
|
|
145
|
-
filter((result) => result !== undefined),
|
|
146
|
-
),
|
|
174
|
+
stateSource!.observable.pipe(filter((permissions) => permissions !== undefined)),
|
|
147
175
|
)
|
|
148
176
|
}
|
|
149
177
|
|
|
150
|
-
|
|
151
|
-
() => getPermissionsState(instance, {actions}),
|
|
152
|
-
[actions, instance],
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
return useSyncExternalStore(subscribe, getCurrent) as DocumentPermissionsResult
|
|
178
|
+
return result as DocumentPermissionsResult
|
|
156
179
|
}
|