@sanity/sdk-react 2.9.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.
- package/dist/index.d.ts +92 -26
- package/dist/index.js +304 -193
- package/dist/index.js.map +1 -1
- package/package.json +9 -11
- package/src/_exports/sdk-react.ts +4 -0
- package/src/components/SDKProvider.tsx +36 -8
- package/src/components/SanityApp.tsx +2 -2
- package/src/components/auth/AuthBoundary.tsx +8 -1
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +100 -56
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/context/ResourceProvider.tsx +5 -4
- package/src/context/ResourcesContext.tsx +7 -0
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/dashboard/useDispatchIntent.test.ts +6 -6
- package/src/hooks/dashboard/useDispatchIntent.ts +6 -6
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +15 -15
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +13 -13
- package/src/hooks/document/useApplyDocumentActions.test.ts +10 -10
- package/src/hooks/document/useApplyDocumentActions.ts +17 -17
- package/src/hooks/document/useDocument.ts +5 -5
- package/src/hooks/document/useDocumentEvent.ts +4 -4
- package/src/hooks/document/useDocumentPermissions.test.tsx +10 -10
- package/src/hooks/document/useDocumentPermissions.ts +8 -8
- package/src/hooks/document/useDocumentSyncStatus.ts +2 -2
- package/src/hooks/document/useEditDocument.ts +2 -2
- package/src/hooks/documents/useDocuments.ts +9 -6
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +131 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +2 -2
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +9 -8
- package/src/hooks/presence/usePresence.test.tsx +56 -9
- package/src/hooks/presence/usePresence.ts +23 -4
- package/src/hooks/preview/useDocumentPreview.tsx +8 -7
- package/src/hooks/projection/useDocumentProjection.ts +6 -6
- package/src/hooks/query/useQuery.ts +10 -9
- package/src/hooks/releases/useActiveReleases.ts +10 -10
- package/src/hooks/releases/usePerspective.ts +9 -9
- package/src/context/SourcesContext.tsx +0 -7
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +0 -107
|
@@ -176,7 +176,7 @@ describe('useApplyDocumentActions', () => {
|
|
|
176
176
|
}).toThrow(/Mismatched datasets found in actions/)
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
-
it('throws when actions have mismatched
|
|
179
|
+
it('throws when actions have mismatched resources', async () => {
|
|
180
180
|
const {result} = renderHook(() => useApplyDocumentActions())
|
|
181
181
|
expect(() => {
|
|
182
182
|
result.current([
|
|
@@ -184,19 +184,19 @@ describe('useApplyDocumentActions', () => {
|
|
|
184
184
|
type: 'document.edit',
|
|
185
185
|
documentType: 'post',
|
|
186
186
|
documentId: 'abc',
|
|
187
|
-
|
|
187
|
+
resource: {projectId: 'p', dataset: 'd1'},
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
type: 'document.edit',
|
|
191
191
|
documentType: 'post',
|
|
192
192
|
documentId: 'def',
|
|
193
|
-
|
|
193
|
+
resource: {projectId: 'p', dataset: 'd2'},
|
|
194
194
|
},
|
|
195
195
|
])
|
|
196
|
-
}).toThrow(/Mismatched
|
|
196
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
197
197
|
})
|
|
198
198
|
|
|
199
|
-
it('throws when mixing projectId and
|
|
199
|
+
it('throws when mixing projectId and resource (projectId first)', async () => {
|
|
200
200
|
const {result} = renderHook(() => useApplyDocumentActions())
|
|
201
201
|
expect(() => {
|
|
202
202
|
result.current([
|
|
@@ -210,13 +210,13 @@ describe('useApplyDocumentActions', () => {
|
|
|
210
210
|
type: 'document.edit',
|
|
211
211
|
documentType: 'post',
|
|
212
212
|
documentId: 'def',
|
|
213
|
-
|
|
213
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
214
214
|
},
|
|
215
215
|
])
|
|
216
|
-
}).toThrow(/Mismatches between projectId\/dataset options and
|
|
216
|
+
}).toThrow(/Mismatches between projectId\/dataset options and resource/)
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
-
it('throws when mixing
|
|
219
|
+
it('throws when mixing resource and projectId (resource first)', async () => {
|
|
220
220
|
const {result} = renderHook(() => useApplyDocumentActions())
|
|
221
221
|
expect(() => {
|
|
222
222
|
result.current([
|
|
@@ -224,7 +224,7 @@ describe('useApplyDocumentActions', () => {
|
|
|
224
224
|
type: 'document.edit',
|
|
225
225
|
documentType: 'post',
|
|
226
226
|
documentId: 'abc',
|
|
227
|
-
|
|
227
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
228
228
|
},
|
|
229
229
|
{
|
|
230
230
|
type: 'document.edit',
|
|
@@ -233,6 +233,6 @@ describe('useApplyDocumentActions', () => {
|
|
|
233
233
|
projectId: 'p',
|
|
234
234
|
},
|
|
235
235
|
])
|
|
236
|
-
}).toThrow(/Mismatches between projectId\/dataset options and
|
|
236
|
+
}).toThrow(/Mismatches between projectId\/dataset options and resource/)
|
|
237
237
|
})
|
|
238
238
|
})
|
|
@@ -7,12 +7,12 @@ import {
|
|
|
7
7
|
import {type SanityDocument} from 'groq'
|
|
8
8
|
import {useContext} from 'react'
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
11
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
type
|
|
15
|
-
} from '../helpers/
|
|
13
|
+
normalizeResourceOptions,
|
|
14
|
+
type WithResourceNameSupport,
|
|
15
|
+
} from '../helpers/useNormalizedResourceOptions'
|
|
16
16
|
// this import is used in an `{@link useEditDocument}`
|
|
17
17
|
// eslint-disable-next-line unused-imports/no-unused-imports, import/consistent-type-specifier-style
|
|
18
18
|
import type {useEditDocument} from './useEditDocument'
|
|
@@ -29,7 +29,7 @@ interface UseApplyDocumentActions {
|
|
|
29
29
|
action:
|
|
30
30
|
| DocumentAction<TDocumentType, TDataset, TProjectId>
|
|
31
31
|
| DocumentAction<TDocumentType, TDataset, TProjectId>[],
|
|
32
|
-
options?:
|
|
32
|
+
options?: WithResourceNameSupport<ApplyDocumentActionsOptions>,
|
|
33
33
|
) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -218,20 +218,20 @@ interface UseApplyDocumentActions {
|
|
|
218
218
|
*/
|
|
219
219
|
export const useApplyDocumentActions: UseApplyDocumentActions = () => {
|
|
220
220
|
const instance = useSanityInstance()
|
|
221
|
-
const
|
|
221
|
+
const resources = useContext(ResourcesContext)
|
|
222
222
|
|
|
223
223
|
return (actionOrActions, options) => {
|
|
224
224
|
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
|
|
225
|
-
const normalizedOptions = options ?
|
|
225
|
+
const normalizedOptions = options ? normalizeResourceOptions(options, resources) : undefined
|
|
226
226
|
|
|
227
227
|
let projectId
|
|
228
228
|
let dataset
|
|
229
|
-
let
|
|
229
|
+
let resource
|
|
230
230
|
for (const action of actions) {
|
|
231
231
|
if (action.projectId) {
|
|
232
|
-
if (
|
|
232
|
+
if (resource) {
|
|
233
233
|
throw new Error(
|
|
234
|
-
`Mismatches between projectId/dataset options and
|
|
234
|
+
`Mismatches between projectId/dataset options and resource in actions. Found projectId "${action.projectId}" and dataset "${action.dataset}" but expected resource "${resource}".`,
|
|
235
235
|
)
|
|
236
236
|
}
|
|
237
237
|
if (!projectId) projectId = action.projectId
|
|
@@ -251,16 +251,16 @@ export const useApplyDocumentActions: UseApplyDocumentActions = () => {
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
if (action.
|
|
255
|
-
if (!
|
|
256
|
-
if (action.
|
|
254
|
+
if (action.resource) {
|
|
255
|
+
if (!resource) resource = action.resource
|
|
256
|
+
if (action.resource !== resource) {
|
|
257
257
|
throw new Error(
|
|
258
|
-
`Mismatched
|
|
258
|
+
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${action.resource}" but expected "${resource}".`,
|
|
259
259
|
)
|
|
260
260
|
}
|
|
261
261
|
if (projectId || dataset) {
|
|
262
262
|
throw new Error(
|
|
263
|
-
`Mismatches between projectId/dataset options and
|
|
263
|
+
`Mismatches between projectId/dataset options and resource in actions. Found "${action.resource}" but expected project "${projectId}" and dataset "${dataset}".`,
|
|
264
264
|
)
|
|
265
265
|
}
|
|
266
266
|
}
|
|
@@ -277,14 +277,14 @@ export const useApplyDocumentActions: UseApplyDocumentActions = () => {
|
|
|
277
277
|
|
|
278
278
|
return applyDocumentActions(actualInstance, {
|
|
279
279
|
actions,
|
|
280
|
-
|
|
280
|
+
resource,
|
|
281
281
|
...normalizedOptions,
|
|
282
282
|
})
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
return applyDocumentActions(instance, {
|
|
286
286
|
actions,
|
|
287
|
-
|
|
287
|
+
resource,
|
|
288
288
|
...normalizedOptions,
|
|
289
289
|
})
|
|
290
290
|
}
|
|
@@ -4,9 +4,9 @@ import {identity} from 'rxjs'
|
|
|
4
4
|
|
|
5
5
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
} from '../helpers/
|
|
7
|
+
useNormalizedResourceOptions,
|
|
8
|
+
type WithResourceNameSupport,
|
|
9
|
+
} from '../helpers/useNormalizedResourceOptions'
|
|
10
10
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
11
11
|
// used in an `{@link useDocumentProjection}` and `{@link useQuery}`
|
|
12
12
|
// eslint-disable-next-line import/consistent-type-specifier-style, unused-imports/no-unused-imports
|
|
@@ -43,7 +43,7 @@ type UseDocumentOptions<
|
|
|
43
43
|
TDocumentType extends string = string,
|
|
44
44
|
TDataset extends string = string,
|
|
45
45
|
TProjectId extends string = string,
|
|
46
|
-
> =
|
|
46
|
+
> = WithResourceNameSupport<DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>>
|
|
47
47
|
|
|
48
48
|
interface UseDocument {
|
|
49
49
|
/** @internal */
|
|
@@ -240,6 +240,6 @@ interface UseDocument {
|
|
|
240
240
|
*/
|
|
241
241
|
export const useDocument = wrapHookWithData((options: UseDocumentOptions) => {
|
|
242
242
|
useTrackHookUsage('useDocument')
|
|
243
|
-
const normalizedOptions =
|
|
243
|
+
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
244
244
|
return useDocumentValue(normalizedOptions)
|
|
245
245
|
}) as UseDocument
|
|
@@ -2,7 +2,7 @@ import {type DatasetHandle, type DocumentEvent, subscribeDocumentEvents} from '@
|
|
|
2
2
|
import {useCallback, useEffect, useInsertionEffect, useRef} from 'react'
|
|
3
3
|
|
|
4
4
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
|
-
import {
|
|
5
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
6
6
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -79,7 +79,7 @@ export function useDocumentEvent<
|
|
|
79
79
|
): void {
|
|
80
80
|
useTrackHookUsage('useDocumentEvent')
|
|
81
81
|
// Destructure handler and datasetHandle from options
|
|
82
|
-
const normalizedOptions =
|
|
82
|
+
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
83
83
|
const {onEvent, ...datasetHandle} = normalizedOptions
|
|
84
84
|
const ref = useRef(onEvent)
|
|
85
85
|
|
|
@@ -95,7 +95,7 @@ export function useDocumentEvent<
|
|
|
95
95
|
useEffect(() => {
|
|
96
96
|
return subscribeDocumentEvents(instance, {
|
|
97
97
|
eventHandler: stableHandler,
|
|
98
|
-
|
|
98
|
+
resource: datasetHandle.resource,
|
|
99
99
|
})
|
|
100
|
-
}, [instance, datasetHandle.
|
|
100
|
+
}, [instance, datasetHandle.resource, stableHandler])
|
|
101
101
|
}
|
|
@@ -183,19 +183,19 @@ describe('usePermissions', () => {
|
|
|
183
183
|
}).toThrow(/Mismatched datasets found in actions/)
|
|
184
184
|
})
|
|
185
185
|
|
|
186
|
-
it('should throw an error if actions have mismatched
|
|
186
|
+
it('should throw an error if actions have mismatched resources', () => {
|
|
187
187
|
const actions = [
|
|
188
188
|
{
|
|
189
189
|
type: 'document.publish' as const,
|
|
190
190
|
documentId: 'doc1',
|
|
191
191
|
documentType: 'article',
|
|
192
|
-
|
|
192
|
+
resource: {projectId: 'p1', dataset: 'd1'},
|
|
193
193
|
},
|
|
194
194
|
{
|
|
195
195
|
type: 'document.publish' as const,
|
|
196
196
|
documentId: 'doc2',
|
|
197
197
|
documentType: 'article',
|
|
198
|
-
|
|
198
|
+
resource: {projectId: 'p2', dataset: 'd2'},
|
|
199
199
|
},
|
|
200
200
|
]
|
|
201
201
|
|
|
@@ -211,17 +211,17 @@ describe('usePermissions', () => {
|
|
|
211
211
|
</ResourceProvider>
|
|
212
212
|
),
|
|
213
213
|
})
|
|
214
|
-
}).toThrow(/Mismatched
|
|
214
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
215
215
|
})
|
|
216
216
|
|
|
217
|
-
it('should throw an error when mixing projectId and
|
|
217
|
+
it('should throw an error when mixing projectId and resource (projectId first)', () => {
|
|
218
218
|
const actions = [
|
|
219
219
|
mockAction,
|
|
220
220
|
{
|
|
221
221
|
type: 'document.publish' as const,
|
|
222
222
|
documentId: 'doc2',
|
|
223
223
|
documentType: 'article',
|
|
224
|
-
|
|
224
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
225
225
|
},
|
|
226
226
|
]
|
|
227
227
|
|
|
@@ -237,16 +237,16 @@ describe('usePermissions', () => {
|
|
|
237
237
|
</ResourceProvider>
|
|
238
238
|
),
|
|
239
239
|
})
|
|
240
|
-
}).toThrow(/Mismatches between projectId\/dataset options and
|
|
240
|
+
}).toThrow(/Mismatches between projectId\/dataset options and resource/)
|
|
241
241
|
})
|
|
242
242
|
|
|
243
|
-
it('should throw an error when mixing
|
|
243
|
+
it('should throw an error when mixing resource and projectId (resource first)', () => {
|
|
244
244
|
const actions = [
|
|
245
245
|
{
|
|
246
246
|
type: 'document.publish' as const,
|
|
247
247
|
documentId: 'doc1',
|
|
248
248
|
documentType: 'article',
|
|
249
|
-
|
|
249
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
250
250
|
},
|
|
251
251
|
mockAction,
|
|
252
252
|
]
|
|
@@ -263,7 +263,7 @@ describe('usePermissions', () => {
|
|
|
263
263
|
</ResourceProvider>
|
|
264
264
|
),
|
|
265
265
|
})
|
|
266
|
-
}).toThrow(/Mismatches between projectId\/dataset options and
|
|
266
|
+
}).toThrow(/Mismatches between projectId\/dataset options and resource/)
|
|
267
267
|
})
|
|
268
268
|
|
|
269
269
|
it('should wait for permissions to be ready before rendering', async () => {
|
|
@@ -92,13 +92,13 @@ export function useDocumentPermissions(
|
|
|
92
92
|
// if actions is an array, we need to check that all actions belong to the same project and dataset
|
|
93
93
|
let projectId
|
|
94
94
|
let dataset
|
|
95
|
-
let
|
|
95
|
+
let resource
|
|
96
96
|
|
|
97
97
|
for (const action of actions) {
|
|
98
98
|
if (action.projectId) {
|
|
99
|
-
if (
|
|
99
|
+
if (resource) {
|
|
100
100
|
throw new Error(
|
|
101
|
-
`Mismatches between projectId/dataset options and
|
|
101
|
+
`Mismatches between projectId/dataset options and resource in actions. Found projectId "${action.projectId}" and dataset "${action.dataset}" but expected resource "${resource}".`,
|
|
102
102
|
)
|
|
103
103
|
}
|
|
104
104
|
if (!projectId) projectId = action.projectId
|
|
@@ -118,16 +118,16 @@ export function useDocumentPermissions(
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
if (action.
|
|
122
|
-
if (!
|
|
123
|
-
if (action.
|
|
121
|
+
if (action.resource) {
|
|
122
|
+
if (!resource) resource = action.resource
|
|
123
|
+
if (action.resource !== resource) {
|
|
124
124
|
throw new Error(
|
|
125
|
-
`Mismatched
|
|
125
|
+
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${action.resource}" but expected "${resource}".`,
|
|
126
126
|
)
|
|
127
127
|
}
|
|
128
128
|
if (projectId || dataset) {
|
|
129
129
|
throw new Error(
|
|
130
|
-
`Mismatches between projectId/dataset options and
|
|
130
|
+
`Mismatches between projectId/dataset options and resource in actions. Found "${action.resource}" but expected project "${projectId}" and dataset "${dataset}".`,
|
|
131
131
|
)
|
|
132
132
|
}
|
|
133
133
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import {identity} from 'rxjs'
|
|
10
10
|
|
|
11
11
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
12
|
-
import {
|
|
12
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
13
13
|
|
|
14
14
|
type UseDocumentSyncStatus = {
|
|
15
15
|
/**
|
|
@@ -65,6 +65,6 @@ const useDocumentSyncStatusValue = createStateSourceHook({
|
|
|
65
65
|
export const useDocumentSyncStatus: UseDocumentSyncStatus = (
|
|
66
66
|
options: DocumentOptions<string | undefined>,
|
|
67
67
|
) => {
|
|
68
|
-
const normalizedOptions =
|
|
68
|
+
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
69
69
|
return useDocumentSyncStatusValue(normalizedOptions)
|
|
70
70
|
}
|
|
@@ -10,7 +10,7 @@ import {type SanityDocument} from 'groq'
|
|
|
10
10
|
import {useCallback} from 'react'
|
|
11
11
|
|
|
12
12
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
13
|
-
import {
|
|
13
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
14
14
|
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
15
15
|
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
16
16
|
|
|
@@ -288,7 +288,7 @@ export function useEditDocument({
|
|
|
288
288
|
}: DocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
|
|
289
289
|
const instance = useSanityInstance(doc)
|
|
290
290
|
trackHookUsage(instance, 'useEditDocument')
|
|
291
|
-
const normalizedDoc =
|
|
291
|
+
const normalizedDoc = useNormalizedResourceOptions(doc)
|
|
292
292
|
|
|
293
293
|
const apply = useApplyDocumentActions()
|
|
294
294
|
const isDocumentReady = useCallback(
|
|
@@ -5,8 +5,7 @@ import {
|
|
|
5
5
|
type QueryOptions,
|
|
6
6
|
} from '@sanity/sdk'
|
|
7
7
|
import {type SortOrderingItem} from '@sanity/types'
|
|
8
|
-
import {
|
|
9
|
-
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
8
|
+
import {useCallback, useMemo, useState} from 'react'
|
|
10
9
|
|
|
11
10
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
11
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
@@ -230,9 +229,11 @@ export function useDocuments<
|
|
|
230
229
|
types: documentTypes,
|
|
231
230
|
...options,
|
|
232
231
|
})
|
|
233
|
-
|
|
232
|
+
const [prevKey, setPrevKey] = useState(key)
|
|
233
|
+
if (prevKey !== key) {
|
|
234
|
+
setPrevKey(key)
|
|
234
235
|
setLimit(batchSize)
|
|
235
|
-
}
|
|
236
|
+
}
|
|
236
237
|
|
|
237
238
|
const filterClause = useMemo(() => {
|
|
238
239
|
const conditions: string[] = []
|
|
@@ -281,9 +282,11 @@ export function useDocuments<
|
|
|
281
282
|
query: `{"count":${countQuery},"data":${dataQuery}}`,
|
|
282
283
|
params: {
|
|
283
284
|
...params,
|
|
285
|
+
// these are passed back to the user as part of each document handle
|
|
284
286
|
__handle: {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
+
projectId: options.projectId ?? instance.config.projectId,
|
|
288
|
+
dataset: options.dataset ?? instance.config.dataset,
|
|
289
|
+
perspective: options.perspective ?? instance.config.perspective,
|
|
287
290
|
},
|
|
288
291
|
__types: documentTypes,
|
|
289
292
|
},
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {type DocumentResource} from '@sanity/sdk'
|
|
2
|
+
import {useContext} from 'react'
|
|
3
|
+
|
|
4
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adds React hook support (resourceName resolution) to core types.
|
|
8
|
+
* This wrapper allows hooks to accept `resourceName` as a convenience,
|
|
9
|
+
* which is then resolved to a `DocumentResource` at the React layer.
|
|
10
|
+
* For now, we are trying to avoid resource name resolution in core --
|
|
11
|
+
* functions having resources explicitly passed will reduce complexity.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam T - The core type to extend (must have optional `resource` field)
|
|
14
|
+
* @beta
|
|
15
|
+
*/
|
|
16
|
+
export type WithResourceNameSupport<T extends {resource?: DocumentResource}> = T & {
|
|
17
|
+
/**
|
|
18
|
+
* Optional name of a resource to resolve from context.
|
|
19
|
+
* If provided, will be resolved to a `DocumentResource` via `ResourcesContext`.
|
|
20
|
+
* @beta
|
|
21
|
+
*/
|
|
22
|
+
resourceName?: string
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Use `resourceName` instead.
|
|
25
|
+
* @beta
|
|
26
|
+
*/
|
|
27
|
+
sourceName?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pure function that normalizes options by resolving `resourceName` to a `DocumentResource`
|
|
32
|
+
* using the provided resources map. Use this when options are only available at call time
|
|
33
|
+
* (e.g. inside a callback) and you cannot call the {@link useNormalizedResourceOptions} hook.
|
|
34
|
+
*
|
|
35
|
+
* @typeParam T - The options type (must include optional resource field)
|
|
36
|
+
* @param options - Options that may include `resourceName` and/or `resource`
|
|
37
|
+
* @param resources - Map of resource names to DocumentResource (e.g. from ResourcesContext)
|
|
38
|
+
* @returns Normalized options with `resourceName` removed and `resource` resolved
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeResourceOptions<
|
|
42
|
+
T extends {
|
|
43
|
+
resource?: DocumentResource
|
|
44
|
+
resourceName?: string
|
|
45
|
+
source?: DocumentResource
|
|
46
|
+
sourceName?: string
|
|
47
|
+
},
|
|
48
|
+
>(
|
|
49
|
+
options: T,
|
|
50
|
+
resources: Record<string, DocumentResource>,
|
|
51
|
+
): Omit<T, 'resourceName' | 'source' | 'sourceName'> {
|
|
52
|
+
const {resourceName, sourceName, source, ...rest} = options
|
|
53
|
+
|
|
54
|
+
// Coalesce deprecated aliases to their canonical equivalents
|
|
55
|
+
const effectiveResourceName = resourceName ?? sourceName
|
|
56
|
+
const effectiveResource = options.resource ?? source
|
|
57
|
+
|
|
58
|
+
if (!effectiveResourceName && !effectiveResource) {
|
|
59
|
+
return rest as Omit<T, 'resourceName' | 'source' | 'sourceName'>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hasNameKey = Object.hasOwn(options, 'resourceName') || Object.hasOwn(options, 'sourceName')
|
|
63
|
+
const hasResourceKey = Object.hasOwn(options, 'resource') || Object.hasOwn(options, 'source')
|
|
64
|
+
|
|
65
|
+
if (hasNameKey && hasResourceKey) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Resource name ${JSON.stringify(effectiveResourceName)} and resource ${JSON.stringify(effectiveResource)} cannot be used together.`,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let resolvedResource: DocumentResource | undefined
|
|
72
|
+
|
|
73
|
+
if (effectiveResource) {
|
|
74
|
+
resolvedResource = effectiveResource
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (effectiveResourceName && !Object.hasOwn(resources, effectiveResourceName)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`There's no resource named ${JSON.stringify(effectiveResourceName)} in context. Please use <ResourceProvider>.`,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (effectiveResourceName && resources[effectiveResourceName]) {
|
|
84
|
+
resolvedResource = resources[effectiveResourceName]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
...rest,
|
|
89
|
+
resource: resolvedResource,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Normalizes hook options by resolving `resourceName` to a `DocumentResource`.
|
|
95
|
+
* This hook ensures that options passed to core layer functions only contain
|
|
96
|
+
* `resource` (never `resourceName`), preventing duplicate cache keys and maintaining
|
|
97
|
+
* clean separation between React and core layers.
|
|
98
|
+
*
|
|
99
|
+
* @typeParam T - The options type (must include optional resource field)
|
|
100
|
+
* @param options - Hook options that may include `resourceName` and/or `resource`
|
|
101
|
+
* @returns Normalized options with `resourceName` removed and `resource` resolved
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* Resolution priority:
|
|
105
|
+
* 1. If `resourceName` is provided, resolves it via `ResourcesContext` and uses that
|
|
106
|
+
* 2. Otherwise, uses the inline `resource` if provided
|
|
107
|
+
* 3. If neither is provided, returns options without a resource field
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```tsx
|
|
111
|
+
* function useQuery(options: WithResourceNameSupport<QueryOptions>) {
|
|
112
|
+
* const instance = useSanityInstance(options)
|
|
113
|
+
* const normalized = useNormalizedOptions(options)
|
|
114
|
+
* // normalized now has resource but never resourceName
|
|
115
|
+
* const queryKey = getQueryKey(normalized)
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @beta
|
|
120
|
+
*/
|
|
121
|
+
export function useNormalizedResourceOptions<
|
|
122
|
+
T extends {
|
|
123
|
+
resource?: DocumentResource
|
|
124
|
+
resourceName?: string
|
|
125
|
+
source?: DocumentResource
|
|
126
|
+
sourceName?: string
|
|
127
|
+
},
|
|
128
|
+
>(options: T): Omit<T, 'resourceName' | 'source' | 'sourceName'> {
|
|
129
|
+
const resources = useContext(ResourcesContext)
|
|
130
|
+
return normalizeResourceOptions(options, resources)
|
|
131
|
+
}
|
|
@@ -18,8 +18,8 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
18
18
|
*/
|
|
19
19
|
export function useTrackHookUsage(hookName: string): void {
|
|
20
20
|
const instance = useSanityInstance()
|
|
21
|
-
const tracked = useRef(
|
|
22
|
-
if (
|
|
21
|
+
const tracked = useRef<true | null>(null)
|
|
22
|
+
if (tracked.current === null) {
|
|
23
23
|
tracked.current = true
|
|
24
24
|
trackHookMounted(instance, hookName)
|
|
25
25
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {createGroqSearchFilter, type DocumentHandle, type QueryOptions} from '@sanity/sdk'
|
|
2
2
|
import {type SortOrderingItem} from '@sanity/types'
|
|
3
|
-
import {
|
|
4
|
-
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
3
|
+
import {useCallback, useMemo, useState} from 'react'
|
|
5
4
|
|
|
6
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
6
|
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
@@ -243,11 +242,12 @@ export function usePaginatedDocuments<
|
|
|
243
242
|
const instance = useSanityInstance(options)
|
|
244
243
|
const [pageIndex, setPageIndex] = useState(0)
|
|
245
244
|
const key = JSON.stringify({filter, search, params, orderings, pageSize})
|
|
246
|
-
// Reset
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
// Reset pageIndex to 0 whenever any query parameter changes.
|
|
246
|
+
const [prevKey, setPrevKey] = useState(key)
|
|
247
|
+
if (prevKey !== key) {
|
|
248
|
+
setPrevKey(key)
|
|
249
249
|
setPageIndex(0)
|
|
250
|
-
}
|
|
250
|
+
}
|
|
251
251
|
|
|
252
252
|
const startIndex = pageIndex * pageSize
|
|
253
253
|
const endIndex = (pageIndex + 1) * pageSize
|
|
@@ -303,8 +303,9 @@ export function usePaginatedDocuments<
|
|
|
303
303
|
...params,
|
|
304
304
|
__types: documentTypes,
|
|
305
305
|
__handle: {
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
projectId: options.projectId ?? instance.config.projectId,
|
|
307
|
+
dataset: options.dataset ?? instance.config.dataset,
|
|
308
|
+
perspective: options.perspective ?? instance.config.perspective,
|
|
308
309
|
},
|
|
309
310
|
},
|
|
310
311
|
})
|
|
@@ -6,14 +6,17 @@ import {describe, expect, it, vi} from 'vitest'
|
|
|
6
6
|
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
7
7
|
import {usePresence} from './usePresence'
|
|
8
8
|
|
|
9
|
-
vi.mock('@sanity/sdk', () =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
10
|
+
const actual = await importOriginal<typeof import('@sanity/sdk')>()
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
getPresence: vi.fn(),
|
|
14
|
+
createSanityInstance: vi.fn(() => ({
|
|
15
|
+
isDisposed: vi.fn(() => false),
|
|
16
|
+
dispose: vi.fn(),
|
|
17
|
+
})),
|
|
18
|
+
}
|
|
19
|
+
})
|
|
17
20
|
|
|
18
21
|
describe('usePresence', () => {
|
|
19
22
|
it('should return presence locations and update when the store changes', () => {
|
|
@@ -59,7 +62,10 @@ describe('usePresence', () => {
|
|
|
59
62
|
|
|
60
63
|
const {result, unmount} = renderHook(() => usePresence(), {
|
|
61
64
|
wrapper: ({children}) => (
|
|
62
|
-
<ResourceProvider
|
|
65
|
+
<ResourceProvider
|
|
66
|
+
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
67
|
+
fallback={null}
|
|
68
|
+
>
|
|
63
69
|
{children}
|
|
64
70
|
</ResourceProvider>
|
|
65
71
|
),
|
|
@@ -80,4 +86,45 @@ describe('usePresence', () => {
|
|
|
80
86
|
expect(result.current.locations).toEqual(updatedLocations)
|
|
81
87
|
unmount()
|
|
82
88
|
})
|
|
89
|
+
|
|
90
|
+
it('should throw an error when used with a media library resource', () => {
|
|
91
|
+
expect(() => {
|
|
92
|
+
renderHook(() => usePresence({resource: {mediaLibraryId: 'ml123'}}), {
|
|
93
|
+
wrapper: ({children}) => (
|
|
94
|
+
<ResourceProvider
|
|
95
|
+
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
96
|
+
fallback={null}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</ResourceProvider>
|
|
100
|
+
),
|
|
101
|
+
})
|
|
102
|
+
}).toThrow('usePresence() does not support media library resources')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should work with a dataset resource', () => {
|
|
106
|
+
const mockPresenceSource = {
|
|
107
|
+
getCurrent: vi.fn().mockReturnValue([]),
|
|
108
|
+
subscribe: vi.fn(() => () => {}),
|
|
109
|
+
observable: NEVER,
|
|
110
|
+
}
|
|
111
|
+
vi.mocked(getPresence).mockReturnValue(mockPresenceSource)
|
|
112
|
+
|
|
113
|
+
const {result, unmount} = renderHook(
|
|
114
|
+
() => usePresence({resource: {projectId: 'test-project', dataset: 'test-dataset'}}),
|
|
115
|
+
{
|
|
116
|
+
wrapper: ({children}) => (
|
|
117
|
+
<ResourceProvider
|
|
118
|
+
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
119
|
+
fallback={null}
|
|
120
|
+
>
|
|
121
|
+
{children}
|
|
122
|
+
</ResourceProvider>
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
expect(result.current.locations).toEqual([])
|
|
128
|
+
unmount()
|
|
129
|
+
})
|
|
83
130
|
})
|