@sanity/sdk-react 2.7.0 → 3.0.0-rc.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/README.md +125 -63
- package/dist/index.d.ts +381 -571
- package/dist/index.js +450 -366
- package/dist/index.js.map +1 -1
- package/package.json +6 -8
- package/src/_exports/index.ts +4 -0
- package/src/_exports/sdk-react.ts +16 -0
- package/src/components/SDKProvider.test.tsx +23 -58
- package/src/components/SDKProvider.tsx +38 -30
- package/src/components/SanityApp.test.tsx +12 -68
- package/src/components/SanityApp.tsx +88 -65
- package/src/components/auth/AuthBoundary.test.tsx +11 -26
- package/src/components/auth/LoginError.test.tsx +5 -0
- package/src/components/auth/LoginError.tsx +23 -2
- package/src/config/handles.ts +53 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +27 -10
- package/src/context/DefaultResourceContext.ts +10 -0
- package/src/context/PerspectiveContext.ts +12 -0
- package/src/context/ResourceProvider.test.tsx +99 -19
- package/src/context/ResourceProvider.tsx +103 -37
- package/src/context/ResourcesContext.tsx +7 -0
- package/src/context/SDKStudioContext.test.tsx +33 -28
- package/src/context/SDKStudioContext.ts +6 -0
- package/src/context/renderSanityApp.test.tsx +49 -151
- package/src/context/renderSanityApp.tsx +8 -12
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +56 -19
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +8 -2
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +32 -8
- package/src/hooks/client/useClient.test.tsx +4 -1
- package/src/hooks/client/useClient.ts +0 -1
- package/src/hooks/context/useDefaultResource.test.tsx +25 -0
- package/src/hooks/context/useDefaultResource.ts +30 -0
- package/src/hooks/context/useSanityInstance.test.tsx +2 -140
- package/src/hooks/context/useSanityInstance.ts +9 -53
- package/src/hooks/dashboard/useDispatchIntent.test.ts +24 -15
- package/src/hooks/dashboard/useDispatchIntent.ts +7 -7
- package/src/hooks/dashboard/useManageFavorite.test.tsx +34 -94
- package/src/hooks/dashboard/useManageFavorite.ts +16 -10
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +7 -5
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +6 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +2 -0
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -1
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +17 -38
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +12 -19
- package/src/hooks/datasets/useDatasets.test.ts +8 -22
- package/src/hooks/datasets/useDatasets.ts +8 -16
- package/src/hooks/document/useApplyDocumentActions.test.ts +98 -52
- package/src/hooks/document/useApplyDocumentActions.ts +35 -37
- package/src/hooks/document/useDocument.test.tsx +8 -37
- package/src/hooks/document/useDocument.ts +78 -129
- package/src/hooks/document/useDocumentEvent.test.tsx +7 -19
- package/src/hooks/document/useDocumentEvent.ts +21 -19
- package/src/hooks/document/useDocumentPermissions.test.tsx +75 -84
- package/src/hooks/document/useDocumentPermissions.ts +41 -28
- package/src/hooks/document/useDocumentSyncStatus.test.ts +13 -3
- package/src/hooks/document/useDocumentSyncStatus.ts +19 -14
- package/src/hooks/document/useEditDocument.test.tsx +28 -70
- package/src/hooks/document/useEditDocument.ts +29 -149
- package/src/hooks/documents/useDocuments.test.tsx +44 -64
- package/src/hooks/documents/useDocuments.ts +19 -25
- package/src/hooks/helpers/createCallbackHook.test.tsx +19 -13
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -4
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +65 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +127 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +27 -34
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +19 -20
- package/src/hooks/presence/usePresence.test.tsx +71 -9
- package/src/hooks/presence/usePresence.ts +28 -3
- package/src/hooks/preview/useDocumentPreview.test.tsx +85 -193
- package/src/hooks/preview/useDocumentPreview.tsx +42 -62
- package/src/hooks/projection/useDocumentProjection.test.tsx +9 -37
- package/src/hooks/projection/useDocumentProjection.ts +9 -82
- package/src/hooks/projects/useProject.test.ts +1 -2
- package/src/hooks/projects/useProject.ts +7 -8
- package/src/hooks/query/useQuery.test.tsx +5 -6
- package/src/hooks/query/useQuery.ts +12 -91
- package/src/hooks/releases/useActiveReleases.test.tsx +2 -2
- package/src/hooks/releases/useActiveReleases.ts +25 -13
- package/src/hooks/releases/usePerspective.test.tsx +9 -17
- package/src/hooks/releases/usePerspective.ts +29 -18
- package/src/hooks/users/useUser.test.tsx +9 -3
- package/src/hooks/users/useUser.ts +1 -1
- package/src/hooks/users/useUsers.test.tsx +5 -2
- package/src/hooks/users/useUsers.ts +1 -1
- package/src/context/SourcesContext.tsx +0 -7
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -85
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
|
|
2
|
-
import {
|
|
3
|
-
import {BehaviorSubject, firstValueFrom} from 'rxjs'
|
|
2
|
+
import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs'
|
|
4
3
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
4
|
|
|
5
|
+
import {act, renderHook, waitFor} from '../../../test/test-utils'
|
|
6
6
|
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
7
7
|
import {useDocumentPermissions} from './useDocumentPermissions'
|
|
8
8
|
|
|
@@ -24,12 +24,12 @@ vi.mock('rxjs', async (importOriginal) => {
|
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
describe('usePermissions', () => {
|
|
27
|
+
const mockResource = {projectId: 'project1', dataset: 'dataset1'}
|
|
27
28
|
const mockAction: DocumentAction = {
|
|
28
29
|
type: 'document.publish',
|
|
29
30
|
documentId: 'doc1',
|
|
30
31
|
documentType: 'article',
|
|
31
|
-
|
|
32
|
-
dataset: 'dataset1',
|
|
32
|
+
resource: mockResource,
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const mockPermissionAllowed: DocumentPermissionsResult = {allowed: true}
|
|
@@ -68,7 +68,8 @@ describe('usePermissions', () => {
|
|
|
68
68
|
|
|
69
69
|
// Set up the getPermissionsState mock
|
|
70
70
|
vi.mocked(getPermissionsState).mockReturnValue({
|
|
71
|
-
observable:
|
|
71
|
+
observable:
|
|
72
|
+
permissionsSubject.asObservable() as unknown as Observable<DocumentPermissionsResult>,
|
|
72
73
|
subscribe: mockSubscribe,
|
|
73
74
|
getCurrent: mockGetCurrent,
|
|
74
75
|
})
|
|
@@ -84,20 +85,13 @@ describe('usePermissions', () => {
|
|
|
84
85
|
permissionsSubject.next(mockPermissionAllowed)
|
|
85
86
|
})
|
|
86
87
|
|
|
87
|
-
const {result} = renderHook(() => useDocumentPermissions(mockAction)
|
|
88
|
-
wrapper: ({children}) => (
|
|
89
|
-
<ResourceProvider
|
|
90
|
-
projectId={mockAction.projectId}
|
|
91
|
-
dataset={mockAction.dataset}
|
|
92
|
-
fallback={null}
|
|
93
|
-
>
|
|
94
|
-
{children}
|
|
95
|
-
</ResourceProvider>
|
|
96
|
-
),
|
|
97
|
-
})
|
|
88
|
+
const {result} = renderHook(() => useDocumentPermissions(mockAction))
|
|
98
89
|
|
|
99
90
|
// ResourceProvider handles the instance configuration
|
|
100
|
-
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
91
|
+
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
92
|
+
expect.any(Object),
|
|
93
|
+
expect.objectContaining({actions: [mockAction], resource: mockResource}),
|
|
94
|
+
)
|
|
101
95
|
expect(result.current).toEqual(mockPermissionAllowed)
|
|
102
96
|
})
|
|
103
97
|
|
|
@@ -107,17 +101,7 @@ describe('usePermissions', () => {
|
|
|
107
101
|
permissionsSubject.next(mockPermissionDenied)
|
|
108
102
|
})
|
|
109
103
|
|
|
110
|
-
const {result} = renderHook(() => useDocumentPermissions(mockAction)
|
|
111
|
-
wrapper: ({children}) => (
|
|
112
|
-
<ResourceProvider
|
|
113
|
-
projectId={mockAction.projectId}
|
|
114
|
-
dataset={mockAction.dataset}
|
|
115
|
-
fallback={null}
|
|
116
|
-
>
|
|
117
|
-
{children}
|
|
118
|
-
</ResourceProvider>
|
|
119
|
-
),
|
|
120
|
-
})
|
|
104
|
+
const {result} = renderHook(() => useDocumentPermissions(mockAction))
|
|
121
105
|
|
|
122
106
|
expect(result.current).toEqual(mockPermissionDenied)
|
|
123
107
|
expect(result.current.allowed).toBe(false)
|
|
@@ -128,58 +112,58 @@ describe('usePermissions', () => {
|
|
|
128
112
|
it('should accept an array of actions', () => {
|
|
129
113
|
const actions = [mockAction, {...mockAction, documentId: 'doc2'}]
|
|
130
114
|
|
|
131
|
-
renderHook(() => useDocumentPermissions(actions)
|
|
132
|
-
wrapper: ({children}) => (
|
|
133
|
-
<ResourceProvider
|
|
134
|
-
projectId={mockAction.projectId}
|
|
135
|
-
dataset={mockAction.dataset}
|
|
136
|
-
fallback={null}
|
|
137
|
-
>
|
|
138
|
-
{children}
|
|
139
|
-
</ResourceProvider>
|
|
140
|
-
),
|
|
141
|
-
})
|
|
115
|
+
renderHook(() => useDocumentPermissions(actions))
|
|
142
116
|
|
|
143
|
-
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
117
|
+
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
118
|
+
expect.any(Object),
|
|
119
|
+
expect.objectContaining({actions, resource: mockResource}),
|
|
120
|
+
)
|
|
144
121
|
})
|
|
145
122
|
|
|
146
|
-
it('should throw an error if actions have mismatched
|
|
123
|
+
it('should throw an error if actions have mismatched resources', () => {
|
|
147
124
|
const actions = [
|
|
148
125
|
mockAction,
|
|
149
|
-
{
|
|
126
|
+
{
|
|
127
|
+
...mockAction,
|
|
128
|
+
resource: {projectId: 'different-project', dataset: 'dataset1'},
|
|
129
|
+
documentId: 'doc2',
|
|
130
|
+
},
|
|
150
131
|
]
|
|
151
132
|
|
|
152
133
|
expect(() => {
|
|
153
|
-
renderHook(() => useDocumentPermissions(actions)
|
|
154
|
-
|
|
155
|
-
<ResourceProvider
|
|
156
|
-
projectId={mockAction.projectId}
|
|
157
|
-
dataset={mockAction.dataset}
|
|
158
|
-
fallback={null}
|
|
159
|
-
>
|
|
160
|
-
{children}
|
|
161
|
-
</ResourceProvider>
|
|
162
|
-
),
|
|
163
|
-
})
|
|
164
|
-
}).toThrow(/Mismatched project IDs found in actions/)
|
|
134
|
+
renderHook(() => useDocumentPermissions(actions))
|
|
135
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
165
136
|
})
|
|
166
137
|
|
|
167
138
|
it('should throw an error if actions have mismatched datasets', () => {
|
|
168
|
-
const actions = [
|
|
139
|
+
const actions = [
|
|
140
|
+
mockAction,
|
|
141
|
+
{
|
|
142
|
+
...mockAction,
|
|
143
|
+
resource: {projectId: 'project1', dataset: 'different-dataset'},
|
|
144
|
+
documentId: 'doc2',
|
|
145
|
+
},
|
|
146
|
+
]
|
|
169
147
|
|
|
170
148
|
expect(() => {
|
|
171
|
-
renderHook(() => useDocumentPermissions(actions)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
149
|
+
renderHook(() => useDocumentPermissions(actions))
|
|
150
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should throw an error when mixing different resources', () => {
|
|
154
|
+
const actions = [
|
|
155
|
+
mockAction,
|
|
156
|
+
{
|
|
157
|
+
type: 'document.publish' as const,
|
|
158
|
+
documentId: 'doc2',
|
|
159
|
+
documentType: 'article',
|
|
160
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
161
|
+
},
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
expect(() => {
|
|
165
|
+
renderHook(() => useDocumentPermissions(actions))
|
|
166
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
183
167
|
})
|
|
184
168
|
|
|
185
169
|
it('should wait for permissions to be ready before rendering', async () => {
|
|
@@ -206,11 +190,7 @@ describe('usePermissions', () => {
|
|
|
206
190
|
},
|
|
207
191
|
{
|
|
208
192
|
wrapper: ({children}) => (
|
|
209
|
-
<ResourceProvider
|
|
210
|
-
projectId={mockAction.projectId}
|
|
211
|
-
dataset={mockAction.dataset}
|
|
212
|
-
fallback={null}
|
|
213
|
-
>
|
|
193
|
+
<ResourceProvider resource={mockResource} fallback={null}>
|
|
214
194
|
{children}
|
|
215
195
|
</ResourceProvider>
|
|
216
196
|
),
|
|
@@ -226,27 +206,38 @@ describe('usePermissions', () => {
|
|
|
226
206
|
|
|
227
207
|
// Now it should render properly
|
|
228
208
|
await waitFor(() => {
|
|
229
|
-
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
209
|
+
expect(getPermissionsState).toHaveBeenCalledWith(
|
|
210
|
+
expect.any(Object),
|
|
211
|
+
expect.objectContaining({actions: [mockAction], resource: mockResource}),
|
|
212
|
+
)
|
|
230
213
|
})
|
|
231
214
|
})
|
|
232
215
|
|
|
216
|
+
it('throws when no resource is found from action or context', () => {
|
|
217
|
+
// Provide SanityInstance via ResourceProvider but no resource, so contextResource is undefined
|
|
218
|
+
expect(() => {
|
|
219
|
+
renderHook(
|
|
220
|
+
() =>
|
|
221
|
+
useDocumentPermissions({
|
|
222
|
+
type: 'document.publish',
|
|
223
|
+
documentId: 'doc1',
|
|
224
|
+
documentType: 'article',
|
|
225
|
+
// no resource
|
|
226
|
+
} as never),
|
|
227
|
+
{
|
|
228
|
+
wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
}).toThrow(/resource is required/)
|
|
232
|
+
})
|
|
233
|
+
|
|
233
234
|
it('should react to permission state changes', async () => {
|
|
234
235
|
// Start with permission allowed
|
|
235
236
|
act(() => {
|
|
236
237
|
permissionsSubject.next(mockPermissionAllowed)
|
|
237
238
|
})
|
|
238
239
|
|
|
239
|
-
const {result, rerender} = renderHook(() => useDocumentPermissions(mockAction)
|
|
240
|
-
wrapper: ({children}) => (
|
|
241
|
-
<ResourceProvider
|
|
242
|
-
projectId={mockAction.projectId}
|
|
243
|
-
dataset={mockAction.dataset}
|
|
244
|
-
fallback={null}
|
|
245
|
-
>
|
|
246
|
-
{children}
|
|
247
|
-
</ResourceProvider>
|
|
248
|
-
),
|
|
249
|
-
})
|
|
240
|
+
const {result, rerender} = renderHook(() => useDocumentPermissions(mockAction))
|
|
250
241
|
|
|
251
242
|
expect(result.current).toEqual(mockPermissionAllowed)
|
|
252
243
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
|
|
2
|
-
import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
2
|
+
import {useCallback, useContext, useMemo, useSyncExternalStore} from 'react'
|
|
3
3
|
import {filter, firstValueFrom} from 'rxjs'
|
|
4
4
|
|
|
5
|
+
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
6
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
5
7
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
8
|
+
import {normalizeResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
*
|
|
@@ -84,50 +87,60 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
84
87
|
export function useDocumentPermissions(
|
|
85
88
|
actionOrActions: DocumentAction | DocumentAction[],
|
|
86
89
|
): DocumentPermissionsResult {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
// if actions is an array, we need to check that all actions belong to the same project and dataset
|
|
92
|
-
let projectId
|
|
93
|
-
let dataset
|
|
90
|
+
const instance = useSanityInstance()
|
|
91
|
+
const contextResource = useContext(ResourceContext)
|
|
92
|
+
const resources = useContext(ResourcesContext)
|
|
94
93
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Mismatched project IDs found in actions. All actions must belong to the same project. Found "${action.projectId}" but expected "${projectId}".`,
|
|
94
|
+
const normalizedActions = useMemo(() => {
|
|
95
|
+
return Array.isArray(actionOrActions)
|
|
96
|
+
? actionOrActions.map((action) =>
|
|
97
|
+
normalizeResourceOptions(action, resources, contextResource),
|
|
101
98
|
)
|
|
102
|
-
|
|
99
|
+
: [normalizeResourceOptions(actionOrActions, resources, contextResource)]
|
|
100
|
+
}, [actionOrActions, resources, contextResource])
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
// if actions is an array, we need to check that all actions belong to the same resource
|
|
103
|
+
let resource
|
|
104
|
+
|
|
105
|
+
for (const action of normalizedActions) {
|
|
106
|
+
if (action.resource) {
|
|
107
|
+
if (!resource) resource = action.resource
|
|
108
|
+
if (action.resource !== resource) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
|
|
111
|
+
)
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
const
|
|
116
|
+
const effectiveResource = resource ?? contextResource
|
|
117
|
+
|
|
118
|
+
if (!effectiveResource) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
'No resource found. Provide a resource via the action handle or wrap with a resource context.',
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const permissionsOptions = useMemo(
|
|
125
|
+
() => ({resource: effectiveResource, actions: normalizedActions as DocumentAction[]}),
|
|
126
|
+
[effectiveResource, normalizedActions],
|
|
127
|
+
)
|
|
128
|
+
|
|
116
129
|
const isDocumentReady = useCallback(
|
|
117
|
-
() => getPermissionsState(instance,
|
|
118
|
-
[
|
|
130
|
+
() => getPermissionsState(instance, permissionsOptions).getCurrent() !== undefined,
|
|
131
|
+
[permissionsOptions, instance],
|
|
119
132
|
)
|
|
120
133
|
if (!isDocumentReady()) {
|
|
121
134
|
throw firstValueFrom(
|
|
122
|
-
getPermissionsState(instance,
|
|
135
|
+
getPermissionsState(instance, permissionsOptions).observable.pipe(
|
|
123
136
|
filter((result) => result !== undefined),
|
|
124
137
|
),
|
|
125
138
|
)
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
const {subscribe, getCurrent} = useMemo(
|
|
129
|
-
() => getPermissionsState(instance,
|
|
130
|
-
[
|
|
142
|
+
() => getPermissionsState(instance, permissionsOptions),
|
|
143
|
+
[permissionsOptions, instance],
|
|
131
144
|
)
|
|
132
145
|
|
|
133
146
|
return useSyncExternalStore(subscribe, getCurrent) as DocumentPermissionsResult
|
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import {getDocumentSyncStatus} from '@sanity/sdk'
|
|
2
2
|
import {describe, it} from 'vitest'
|
|
3
3
|
|
|
4
|
+
import {renderHook} from '../../../test/test-utils'
|
|
4
5
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
5
6
|
|
|
6
7
|
const mockHook = vi.fn()
|
|
7
8
|
vi.mock('../helpers/createStateSourceHook', () => ({createStateSourceHook: vi.fn(() => mockHook)}))
|
|
8
|
-
vi.mock('@sanity/sdk', () =>
|
|
9
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
10
|
+
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
11
|
+
return {
|
|
12
|
+
...original,
|
|
13
|
+
getDocumentSyncStatus: vi.fn(),
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
vi.mock('../context/useSanityInstance')
|
|
9
18
|
|
|
10
19
|
describe('useDocumentSyncStatus', () => {
|
|
11
20
|
it('calls `createStateSourceHook` with `getDocumentSyncStatus`', async () => {
|
|
12
21
|
const {useDocumentSyncStatus} = await import('./useDocumentSyncStatus')
|
|
22
|
+
renderHook(() => useDocumentSyncStatus({documentId: '1', documentType: 'test'}))
|
|
13
23
|
expect(createStateSourceHook).toHaveBeenCalledWith(
|
|
14
24
|
expect.objectContaining({
|
|
15
25
|
getState: getDocumentSyncStatus,
|
|
16
26
|
shouldSuspend: expect.any(Function),
|
|
17
27
|
suspender: expect.any(Function),
|
|
18
|
-
getConfig: expect.any(Function),
|
|
19
28
|
}),
|
|
20
29
|
)
|
|
21
|
-
|
|
30
|
+
// Verify that the hook was created and can be called
|
|
31
|
+
expect(mockHook).toHaveBeenCalled()
|
|
22
32
|
})
|
|
23
33
|
})
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type DocumentHandle,
|
|
2
|
+
type DocumentHandle as StrictDocumentHandle,
|
|
3
3
|
getDocumentSyncStatus,
|
|
4
4
|
resolveDocument,
|
|
5
5
|
type SanityInstance,
|
|
6
6
|
type StateSource,
|
|
7
7
|
} from '@sanity/sdk'
|
|
8
|
-
import {identity} from 'rxjs'
|
|
9
8
|
|
|
9
|
+
import {type DocumentHandle} from '../../config/handles'
|
|
10
10
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
11
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
11
12
|
|
|
12
13
|
type UseDocumentSyncStatus = {
|
|
13
14
|
/**
|
|
14
15
|
* Exposes the document's sync status between local and remote document states.
|
|
15
16
|
*
|
|
16
17
|
* @category Documents
|
|
17
|
-
* @param doc - The document handle to get sync status for. If you pass a `
|
|
18
|
-
* the document will be read from the specified
|
|
19
|
-
* the
|
|
18
|
+
* @param doc - The document handle to get sync status for. If you pass a `resource` in the handle,
|
|
19
|
+
* the document will be read from the specified resource. If no `resource` is provided,
|
|
20
|
+
* the resource will be resolved from context.
|
|
20
21
|
* @returns `true` if local changes are synced with remote, `false` if changes are pending. Note: Suspense handles loading states.
|
|
21
22
|
* @example Show sync status indicator
|
|
22
23
|
* ```tsx
|
|
@@ -45,17 +46,21 @@ type UseDocumentSyncStatus = {
|
|
|
45
46
|
(doc: DocumentHandle): boolean
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
* @public
|
|
50
|
-
* @function
|
|
51
|
-
*/
|
|
52
|
-
export const useDocumentSyncStatus: UseDocumentSyncStatus = createStateSourceHook({
|
|
49
|
+
const useDocumentSyncStatusValue = createStateSourceHook({
|
|
53
50
|
getState: getDocumentSyncStatus as (
|
|
54
51
|
instance: SanityInstance,
|
|
55
|
-
doc:
|
|
52
|
+
doc: StrictDocumentHandle,
|
|
56
53
|
) => StateSource<boolean>,
|
|
57
|
-
shouldSuspend: (instance, doc:
|
|
54
|
+
shouldSuspend: (instance, doc: StrictDocumentHandle) =>
|
|
58
55
|
getDocumentSyncStatus(instance, doc).getCurrent() === undefined,
|
|
59
|
-
suspender: (instance, doc:
|
|
60
|
-
getConfig: identity,
|
|
56
|
+
suspender: (instance, doc: StrictDocumentHandle) => resolveDocument(instance, doc),
|
|
61
57
|
})
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @public
|
|
61
|
+
* @function
|
|
62
|
+
*/
|
|
63
|
+
export const useDocumentSyncStatus: UseDocumentSyncStatus = (options: DocumentHandle) => {
|
|
64
|
+
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
65
|
+
return useDocumentSyncStatusValue(normalizedOptions)
|
|
66
|
+
}
|
|
@@ -7,10 +7,9 @@ import {
|
|
|
7
7
|
type StateSource,
|
|
8
8
|
} from '@sanity/sdk'
|
|
9
9
|
import {type SanityDocument} from '@sanity/types'
|
|
10
|
-
import {renderHook} from '@testing-library/react'
|
|
11
10
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
12
11
|
|
|
13
|
-
import {
|
|
12
|
+
import {renderHook} from '../../../test/test-utils'
|
|
14
13
|
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
15
14
|
import {useEditDocument} from './useEditDocument'
|
|
16
15
|
|
|
@@ -40,7 +39,13 @@ const doc = {
|
|
|
40
39
|
const docHandle = createDocumentHandle({
|
|
41
40
|
documentId: 'doc1',
|
|
42
41
|
documentType: 'book',
|
|
42
|
+
resource: {projectId: 'test-project', dataset: 'test-dataset'},
|
|
43
43
|
})
|
|
44
|
+
const normalizedDocHandle = {
|
|
45
|
+
documentId: 'doc1',
|
|
46
|
+
documentType: 'book',
|
|
47
|
+
resource: {projectId: 'test-project', dataset: 'test-dataset'},
|
|
48
|
+
}
|
|
44
49
|
|
|
45
50
|
// Define a single generic TestDocument type
|
|
46
51
|
interface Book extends SanityDocument {
|
|
@@ -50,16 +55,6 @@ interface Book extends SanityDocument {
|
|
|
50
55
|
title?: string
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
// Scope the TestDocument type to the project/datasets used in tests
|
|
54
|
-
type AllTestSchemaTypes = Book
|
|
55
|
-
|
|
56
|
-
// Augment the 'groq' module
|
|
57
|
-
declare module 'groq' {
|
|
58
|
-
interface SanitySchemas {
|
|
59
|
-
'default:default': AllTestSchemaTypes
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
describe('useEditDocument hook', () => {
|
|
64
59
|
beforeEach(() => {
|
|
65
60
|
vi.clearAllMocks()
|
|
@@ -76,16 +71,10 @@ describe('useEditDocument hook', () => {
|
|
|
76
71
|
const apply = vi.fn().mockResolvedValue({transactionId: 'tx1'})
|
|
77
72
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
78
73
|
|
|
79
|
-
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'})
|
|
80
|
-
wrapper: ({children}) => (
|
|
81
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
82
|
-
{children}
|
|
83
|
-
</ResourceProvider>
|
|
84
|
-
),
|
|
85
|
-
})
|
|
74
|
+
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
|
|
86
75
|
const promise = result.current('newValue')
|
|
87
|
-
expect(editDocument).toHaveBeenCalledWith(
|
|
88
|
-
expect(apply).toHaveBeenCalledWith(editDocument(
|
|
76
|
+
expect(editDocument).toHaveBeenCalledWith(normalizedDocHandle, {set: {foo: 'newValue'}})
|
|
77
|
+
expect(apply).toHaveBeenCalledWith(editDocument(normalizedDocHandle, {set: {foo: 'newValue'}}))
|
|
89
78
|
const actionsResult = await promise
|
|
90
79
|
expect(actionsResult).toEqual({transactionId: 'tx1'})
|
|
91
80
|
})
|
|
@@ -103,15 +92,9 @@ describe('useEditDocument hook', () => {
|
|
|
103
92
|
const apply = vi.fn().mockResolvedValue({transactionId: 'tx2'})
|
|
104
93
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
105
94
|
|
|
106
|
-
const {result} = renderHook(() => useEditDocument(docHandle)
|
|
107
|
-
wrapper: ({children}) => (
|
|
108
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
109
|
-
{children}
|
|
110
|
-
</ResourceProvider>
|
|
111
|
-
),
|
|
112
|
-
})
|
|
95
|
+
const {result} = renderHook(() => useEditDocument(docHandle))
|
|
113
96
|
const promise = result.current({...doc, foo: 'baz', extra: 'old', _id: 'doc1'})
|
|
114
|
-
expect(apply).toHaveBeenCalledWith([editDocument(
|
|
97
|
+
expect(apply).toHaveBeenCalledWith([editDocument(normalizedDocHandle, {set: {foo: 'baz'}})])
|
|
115
98
|
const actionsResult = await promise
|
|
116
99
|
expect(actionsResult).toEqual({transactionId: 'tx2'})
|
|
117
100
|
})
|
|
@@ -127,16 +110,12 @@ describe('useEditDocument hook', () => {
|
|
|
127
110
|
const apply = vi.fn().mockResolvedValue({transactionId: 'tx3'})
|
|
128
111
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
129
112
|
|
|
130
|
-
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'})
|
|
131
|
-
wrapper: ({children}) => (
|
|
132
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
133
|
-
{children}
|
|
134
|
-
</ResourceProvider>
|
|
135
|
-
),
|
|
136
|
-
})
|
|
113
|
+
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
|
|
137
114
|
const promise = result.current((prev: unknown) => `${prev}Updated`) // 'bar' becomes 'barUpdated'
|
|
138
|
-
expect(editDocument).toHaveBeenCalledWith(
|
|
139
|
-
expect(apply).toHaveBeenCalledWith(
|
|
115
|
+
expect(editDocument).toHaveBeenCalledWith(normalizedDocHandle, {set: {foo: 'barUpdated'}})
|
|
116
|
+
expect(apply).toHaveBeenCalledWith(
|
|
117
|
+
editDocument(normalizedDocHandle, {set: {foo: 'barUpdated'}}),
|
|
118
|
+
)
|
|
140
119
|
const actionsResult = await promise
|
|
141
120
|
expect(actionsResult).toEqual({transactionId: 'tx3'})
|
|
142
121
|
})
|
|
@@ -153,15 +132,9 @@ describe('useEditDocument hook', () => {
|
|
|
153
132
|
const apply = vi.fn().mockResolvedValue({transactionId: 'tx4'})
|
|
154
133
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
155
134
|
|
|
156
|
-
const {result} = renderHook(() => useEditDocument(docHandle)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
{children}
|
|
160
|
-
</ResourceProvider>
|
|
161
|
-
),
|
|
162
|
-
})
|
|
163
|
-
const promise = result.current((prevDoc) => ({...prevDoc, foo: 'baz'}))
|
|
164
|
-
expect(apply).toHaveBeenCalledWith([editDocument(docHandle, {set: {foo: 'baz'}})])
|
|
135
|
+
const {result} = renderHook(() => useEditDocument(docHandle))
|
|
136
|
+
const promise = result.current((prevDoc: Record<string, unknown>) => ({...prevDoc, foo: 'baz'}))
|
|
137
|
+
expect(apply).toHaveBeenCalledWith([editDocument(normalizedDocHandle, {set: {foo: 'baz'}})])
|
|
165
138
|
const actionsResult = await promise
|
|
166
139
|
expect(actionsResult).toEqual({transactionId: 'tx4'})
|
|
167
140
|
})
|
|
@@ -177,13 +150,7 @@ describe('useEditDocument hook', () => {
|
|
|
177
150
|
const fakeApply = vi.fn()
|
|
178
151
|
vi.mocked(useApplyDocumentActions).mockReturnValue(fakeApply)
|
|
179
152
|
|
|
180
|
-
const {result} = renderHook(() => useEditDocument(docHandle)
|
|
181
|
-
wrapper: ({children}) => (
|
|
182
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
183
|
-
{children}
|
|
184
|
-
</ResourceProvider>
|
|
185
|
-
),
|
|
186
|
-
})
|
|
153
|
+
const {result} = renderHook(() => useEditDocument(docHandle))
|
|
187
154
|
expect(() => result.current('notAnObject' as unknown as Book)).toThrowError(
|
|
188
155
|
'No path was provided to `useEditDocument` and the value provided was not a document object.',
|
|
189
156
|
)
|
|
@@ -203,22 +170,13 @@ describe('useEditDocument hook', () => {
|
|
|
203
170
|
vi.mocked(resolveDocument).mockReturnValue(resolveDocPromise)
|
|
204
171
|
|
|
205
172
|
// Render the hook and capture the thrown promise.
|
|
206
|
-
const {result} = renderHook(
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
wrapper: ({children}) => (
|
|
216
|
-
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
217
|
-
{children}
|
|
218
|
-
</ResourceProvider>
|
|
219
|
-
),
|
|
220
|
-
},
|
|
221
|
-
)
|
|
173
|
+
const {result} = renderHook(() => {
|
|
174
|
+
try {
|
|
175
|
+
return useEditDocument(docHandle)
|
|
176
|
+
} catch (e) {
|
|
177
|
+
return e
|
|
178
|
+
}
|
|
179
|
+
})
|
|
222
180
|
|
|
223
181
|
// When the document is not ready, the hook throws the promise from resolveDocument.
|
|
224
182
|
expect(result.current).toBe(resolveDocPromise)
|