@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,4 +1,4 @@
|
|
|
1
|
-
import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
|
|
1
|
+
import {createSanityInstance, type DatasetResource, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {renderHook} from '@testing-library/react'
|
|
3
3
|
import {throwError} from 'rxjs'
|
|
4
4
|
import {describe, expect, it, vi} from 'vitest'
|
|
@@ -17,7 +17,7 @@ describe('createStateSourceHook', () => {
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
it('should create a hook that provides access to state source', () => {
|
|
20
|
-
const mockInstance = createSanityInstance(
|
|
20
|
+
const mockInstance = createSanityInstance()
|
|
21
21
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
22
22
|
|
|
23
23
|
const mockState = {count: 0}
|
|
@@ -39,7 +39,7 @@ describe('createStateSourceHook', () => {
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
it('should recreate state source when params change', () => {
|
|
42
|
-
const mockInstance = createSanityInstance(
|
|
42
|
+
const mockInstance = createSanityInstance()
|
|
43
43
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
44
44
|
|
|
45
45
|
const subscribe = vi.fn()
|
|
@@ -65,14 +65,14 @@ describe('createStateSourceHook', () => {
|
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
it('should recreate state source when instance changes', () => {
|
|
68
|
-
const mockInstance1 = createSanityInstance({projectId: 'p1', dataset: 'd'})
|
|
69
|
-
const mockInstance2 = createSanityInstance({projectId: 'p2', dataset: 'd'})
|
|
68
|
+
const mockInstance1 = createSanityInstance({defaultResource: {projectId: 'p1', dataset: 'd'}})
|
|
69
|
+
const mockInstance2 = createSanityInstance({defaultResource: {projectId: 'p2', dataset: 'd'}})
|
|
70
70
|
|
|
71
71
|
vi.mocked(useSanityInstance).mockReturnValueOnce(mockInstance1)
|
|
72
72
|
|
|
73
73
|
const stateSourceFactory = vi.fn((instance: SanityInstance) => ({
|
|
74
74
|
subscribe: vi.fn(),
|
|
75
|
-
getCurrent: () => instance.config.projectId,
|
|
75
|
+
getCurrent: () => (instance.config.defaultResource as DatasetResource)?.projectId,
|
|
76
76
|
observable: throwError(() => new Error('unexpected usage of observable')),
|
|
77
77
|
}))
|
|
78
78
|
|
|
@@ -89,7 +89,7 @@ describe('createStateSourceHook', () => {
|
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
it('should handle subscription functionality', () => {
|
|
92
|
-
const mockInstance = createSanityInstance(
|
|
92
|
+
const mockInstance = createSanityInstance()
|
|
93
93
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
94
94
|
|
|
95
95
|
const mockSubscribe = vi.fn()
|
|
@@ -110,7 +110,7 @@ describe('createStateSourceHook', () => {
|
|
|
110
110
|
})
|
|
111
111
|
|
|
112
112
|
it('should handle multiple parameters', () => {
|
|
113
|
-
const mockInstance = createSanityInstance(
|
|
113
|
+
const mockInstance = createSanityInstance()
|
|
114
114
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
115
115
|
|
|
116
116
|
const stateSourceFactory = vi.fn(
|
|
@@ -129,7 +129,7 @@ describe('createStateSourceHook', () => {
|
|
|
129
129
|
})
|
|
130
130
|
|
|
131
131
|
it('should throw suspender promise when shouldSuspend is true', () => {
|
|
132
|
-
const mockInstance = createSanityInstance(
|
|
132
|
+
const mockInstance = createSanityInstance()
|
|
133
133
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
134
134
|
|
|
135
135
|
const mockGetState = vi.fn().mockReturnValue({
|
|
@@ -163,7 +163,7 @@ describe('createStateSourceHook', () => {
|
|
|
163
163
|
})
|
|
164
164
|
|
|
165
165
|
it('should not suspend when shouldSuspend returns false', () => {
|
|
166
|
-
const mockInstance = createSanityInstance(
|
|
166
|
+
const mockInstance = createSanityInstance()
|
|
167
167
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
168
168
|
|
|
169
169
|
const mockState = {value: 'test'}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
2
2
|
import {useSyncExternalStore} from 'react'
|
|
3
3
|
|
|
4
4
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -12,18 +12,16 @@ interface CreateStateSourceHookOptions<TParams extends unknown[], TState> {
|
|
|
12
12
|
getState: StateSourceFactory<TParams, TState>
|
|
13
13
|
shouldSuspend?: (instance: SanityInstance, ...params: TParams) => boolean
|
|
14
14
|
suspender?: (instance: SanityInstance, ...params: TParams) => Promise<unknown>
|
|
15
|
-
getConfig?: (...params: TParams) => SanityConfig | undefined
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export function createStateSourceHook<TParams extends unknown[], TState>(
|
|
19
18
|
options: StateSourceFactory<TParams, TState> | CreateStateSourceHookOptions<TParams, TState>,
|
|
20
19
|
): (...params: TParams) => TState {
|
|
21
20
|
const getState = typeof options === 'function' ? options : options.getState
|
|
22
|
-
const getConfig = 'getConfig' in options ? options.getConfig : undefined
|
|
23
21
|
const suspense = 'shouldSuspend' in options && 'suspender' in options ? options : undefined
|
|
24
22
|
|
|
25
23
|
function useHook(...params: TParams) {
|
|
26
|
-
const instance = useSanityInstance(
|
|
24
|
+
const instance = useSanityInstance()
|
|
27
25
|
|
|
28
26
|
if (suspense?.suspender && suspense?.shouldSuspend?.(instance, ...params)) {
|
|
29
27
|
throw suspense.suspender(instance, ...params)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {normalizeResourceOptions} from './useNormalizedResourceOptions'
|
|
4
|
+
|
|
5
|
+
describe('normalizeResourceOptions', () => {
|
|
6
|
+
it('throws when both resource and resourceName are provided together', () => {
|
|
7
|
+
const resource = {projectId: 'p', dataset: 'd'}
|
|
8
|
+
|
|
9
|
+
expect(() =>
|
|
10
|
+
normalizeResourceOptions(
|
|
11
|
+
{resource, resourceName: 'my-resource'},
|
|
12
|
+
{'my-resource': resource},
|
|
13
|
+
undefined,
|
|
14
|
+
),
|
|
15
|
+
).toThrow(/cannot be used together/)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('throws when no resource can be resolved from any source', () => {
|
|
19
|
+
expect(() =>
|
|
20
|
+
normalizeResourceOptions(
|
|
21
|
+
{},
|
|
22
|
+
{}, // no named resources
|
|
23
|
+
undefined, // no context resource
|
|
24
|
+
),
|
|
25
|
+
).toThrow(/resource is required/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('uses explicit resource when provided', () => {
|
|
29
|
+
const resource = {projectId: 'project-a', dataset: 'staging'}
|
|
30
|
+
|
|
31
|
+
const normalized = normalizeResourceOptions(
|
|
32
|
+
{resource},
|
|
33
|
+
{},
|
|
34
|
+
{projectId: 'default', dataset: 'prod'},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
expect(normalized).toEqual({resource: {projectId: 'project-a', dataset: 'staging'}})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('resolves resource from resourceName', () => {
|
|
41
|
+
const resource = {projectId: 'p', dataset: 'd'}
|
|
42
|
+
|
|
43
|
+
const normalized = normalizeResourceOptions(
|
|
44
|
+
{resourceName: 'my-resource'},
|
|
45
|
+
{'my-resource': resource},
|
|
46
|
+
undefined,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
expect(normalized).toEqual({resource})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('throws when resourceName is not found in resources map', () => {
|
|
53
|
+
expect(() => normalizeResourceOptions({resourceName: 'missing'}, {}, undefined)).toThrow(
|
|
54
|
+
/no resource named/,
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('falls back to context resource when neither resource nor resourceName is provided', () => {
|
|
59
|
+
const contextResource = {projectId: 'default', dataset: 'prod'}
|
|
60
|
+
|
|
61
|
+
const normalized = normalizeResourceOptions({}, {}, contextResource)
|
|
62
|
+
|
|
63
|
+
expect(normalized).toEqual({resource: contextResource})
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {type DocumentResource, type PerspectiveHandle} from '@sanity/sdk'
|
|
2
|
+
import {useContext} from 'react'
|
|
3
|
+
|
|
4
|
+
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
5
|
+
import {PerspectiveContext} from '../../context/PerspectiveContext'
|
|
6
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* You should generally prefer to use the React-layer handle types (ResourceHandle, DocumentHandle) from '\@sanity/sdk-react' instead.
|
|
10
|
+
* This type is useful for non-handles (like document actions) that we still want to resolve resources for.
|
|
11
|
+
* Adds React hook support (resourceName resolution) to core types.
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export type WithResourceNameSupport<T> = Omit<T, 'resource'> & {
|
|
15
|
+
resource?: DocumentResource
|
|
16
|
+
/**
|
|
17
|
+
* Optional name of a resource to resolve from context.
|
|
18
|
+
* If provided, will be resolved to a `DocumentResource` via `ResourcesContext`.
|
|
19
|
+
* @beta
|
|
20
|
+
*/
|
|
21
|
+
resourceName?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pure function that normalizes options by resolving `resourceName` to a `DocumentResource`
|
|
26
|
+
* using the provided resources map, and injecting defaults from context when not provided.
|
|
27
|
+
* Use this when options are only available at call time (e.g. inside a callback)
|
|
28
|
+
* and you cannot call the {@link useNormalizedResourceOptions} hook.
|
|
29
|
+
*
|
|
30
|
+
* @typeParam T - The options type (must include optional resource field)
|
|
31
|
+
* @param options - Options that may include `resourceName` and/or `resource`
|
|
32
|
+
* @param resources - Map of resource names to DocumentResource (e.g. from ResourcesContext)
|
|
33
|
+
* @param contextResource - Resource from context (injected by ResourceProvider)
|
|
34
|
+
* @param contextPerspective - Perspective from context (injected by ResourceProvider)
|
|
35
|
+
* @returns Normalized options with `resourceName` removed and defaults injected
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeResourceOptions<
|
|
39
|
+
T extends {
|
|
40
|
+
resource?: DocumentResource
|
|
41
|
+
resourceName?: string
|
|
42
|
+
perspective?: unknown
|
|
43
|
+
},
|
|
44
|
+
>(
|
|
45
|
+
options: T,
|
|
46
|
+
resources: Record<string, DocumentResource>,
|
|
47
|
+
contextResource?: DocumentResource,
|
|
48
|
+
contextPerspective?: PerspectiveHandle['perspective'],
|
|
49
|
+
): Omit<T, 'resourceName' | 'resource'> & {resource: DocumentResource} {
|
|
50
|
+
const {resourceName, ...rest} = options
|
|
51
|
+
|
|
52
|
+
if (resourceName && Object.hasOwn(options, 'resource')) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Resource name ${JSON.stringify(resourceName)} and resource ${JSON.stringify(options.resource)} cannot be used together.`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let resolvedResource: DocumentResource | undefined = options.resource
|
|
59
|
+
|
|
60
|
+
if (!resolvedResource && resourceName) {
|
|
61
|
+
if (!Object.hasOwn(resources, resourceName)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`There's no resource named ${JSON.stringify(resourceName)} in context. ` +
|
|
64
|
+
'Register it via the resources prop on <SanityApp>.',
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
resolvedResource = resources[resourceName]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!resolvedResource) {
|
|
71
|
+
resolvedResource = contextResource
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (resolvedResource === undefined) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'A resource is required. Provide `resource`, `resourceName`, or ensure a default resource is available from context (e.g. via <ResourceProvider> or <SanityApp>).',
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const resolvedPerspective = Object.hasOwn(options, 'perspective')
|
|
81
|
+
? options.perspective
|
|
82
|
+
: contextPerspective
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...rest,
|
|
86
|
+
resource: resolvedResource,
|
|
87
|
+
...(resolvedPerspective !== undefined && {perspective: resolvedPerspective}),
|
|
88
|
+
} as Omit<T, 'resourceName' | 'resource'> & {resource: DocumentResource}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Normalizes hook options by resolving `resourceName` to a `DocumentResource`
|
|
93
|
+
* and injecting resource/perspective from context.
|
|
94
|
+
*
|
|
95
|
+
* This hook ensures that options passed to core layer functions contain
|
|
96
|
+
* the correct `resource` and `perspective` values, maintaining clean
|
|
97
|
+
* 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 defaults injected
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* Resolution priority for resource:
|
|
105
|
+
* 1. If both `resourceName` and `resource` are provided, throws an error
|
|
106
|
+
* 2. If `resource` is provided, uses it directly
|
|
107
|
+
* 3. If `resourceName` is provided, resolves it via `ResourcesContext`
|
|
108
|
+
* 4. If neither is provided, injects the value from `ResourceContext`
|
|
109
|
+
*
|
|
110
|
+
* Resolution priority for perspective:
|
|
111
|
+
* 1. If `perspective` is explicitly provided in options, uses it
|
|
112
|
+
* 2. Otherwise, injects the value from `PerspectiveContext`
|
|
113
|
+
*
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export function useNormalizedResourceOptions<
|
|
117
|
+
T extends {
|
|
118
|
+
resource?: DocumentResource
|
|
119
|
+
resourceName?: string
|
|
120
|
+
perspective?: unknown
|
|
121
|
+
},
|
|
122
|
+
>(options: T): Omit<T, 'resourceName' | 'resource'> & {resource: DocumentResource} {
|
|
123
|
+
const resources = useContext(ResourcesContext)
|
|
124
|
+
const contextResource = useContext(ResourceContext)
|
|
125
|
+
const contextPerspective = useContext(PerspectiveContext)
|
|
126
|
+
return normalizeResourceOptions(options, resources, contextResource, contextPerspective)
|
|
127
|
+
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {type DatasetResource} from '@sanity/sdk'
|
|
2
2
|
import {evaluateSync, parse, toJS} from 'groq-js'
|
|
3
3
|
import {describe, vi} from 'vitest'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {act, renderHook} from '../../../test/test-utils'
|
|
6
6
|
import {useQuery} from '../query/useQuery'
|
|
7
7
|
import {usePaginatedDocuments} from './usePaginatedDocuments'
|
|
8
8
|
|
|
9
9
|
vi.mock('../query/useQuery')
|
|
10
10
|
|
|
11
11
|
describe('usePaginatedDocuments', () => {
|
|
12
|
-
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
13
|
-
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
14
|
-
{children}
|
|
15
|
-
</ResourceProvider>
|
|
16
|
-
)
|
|
17
|
-
|
|
18
12
|
beforeEach(() => {
|
|
19
13
|
const dataset = [
|
|
20
14
|
{
|
|
@@ -83,16 +77,14 @@ describe('usePaginatedDocuments', () => {
|
|
|
83
77
|
|
|
84
78
|
it('should respect custom page size', () => {
|
|
85
79
|
const customPageSize = 2
|
|
86
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize: customPageSize})
|
|
80
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize: customPageSize}))
|
|
87
81
|
|
|
88
82
|
expect(result.current.pageSize).toBe(customPageSize)
|
|
89
83
|
expect(result.current.data.length).toBeLessThanOrEqual(customPageSize)
|
|
90
84
|
})
|
|
91
85
|
|
|
92
86
|
it('should filter by document type', () => {
|
|
93
|
-
const {result} = renderHook(() => usePaginatedDocuments({filter: '_type == "movie"'})
|
|
94
|
-
wrapper,
|
|
95
|
-
})
|
|
87
|
+
const {result} = renderHook(() => usePaginatedDocuments({filter: '_type == "movie"'}))
|
|
96
88
|
|
|
97
89
|
expect(result.current.data.every((doc) => doc.documentType === 'movie')).toBe(true)
|
|
98
90
|
expect(result.current.count).toBe(5) // 5 movies in the dataset
|
|
@@ -100,20 +92,18 @@ describe('usePaginatedDocuments', () => {
|
|
|
100
92
|
|
|
101
93
|
// groq-js doesn't support search filters yet
|
|
102
94
|
it.skip('should apply search filter', () => {
|
|
103
|
-
const {result} = renderHook(() => usePaginatedDocuments({search: 'inter'})
|
|
95
|
+
const {result} = renderHook(() => usePaginatedDocuments({search: 'inter'}))
|
|
104
96
|
|
|
105
97
|
// Should match "Interstellar"
|
|
106
98
|
expect(result.current.data.some((doc) => doc.documentId === 'movie3')).toBe(true)
|
|
107
99
|
})
|
|
108
100
|
|
|
109
101
|
it('should apply ordering', () => {
|
|
110
|
-
const {result} = renderHook(
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}),
|
|
116
|
-
{wrapper},
|
|
102
|
+
const {result} = renderHook(() =>
|
|
103
|
+
usePaginatedDocuments({
|
|
104
|
+
filter: '_type == "movie"',
|
|
105
|
+
orderings: [{field: 'releaseYear', direction: 'desc'}],
|
|
106
|
+
}),
|
|
117
107
|
)
|
|
118
108
|
|
|
119
109
|
// First item should be the most recent movie (Interstellar, 2014)
|
|
@@ -122,7 +112,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
122
112
|
|
|
123
113
|
it('should calculate pagination values correctly', () => {
|
|
124
114
|
const pageSize = 2
|
|
125
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize})
|
|
115
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
126
116
|
|
|
127
117
|
expect(result.current.currentPage).toBe(1)
|
|
128
118
|
expect(result.current.totalPages).toBe(3) // 6 items with page size 2
|
|
@@ -133,7 +123,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
133
123
|
|
|
134
124
|
it('should navigate to next page', () => {
|
|
135
125
|
const pageSize = 2
|
|
136
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize})
|
|
126
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
137
127
|
|
|
138
128
|
expect(result.current.currentPage).toBe(1)
|
|
139
129
|
expect(result.current.data.length).toBe(pageSize)
|
|
@@ -149,7 +139,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
149
139
|
|
|
150
140
|
it('should navigate to previous page', () => {
|
|
151
141
|
const pageSize = 2
|
|
152
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize})
|
|
142
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
153
143
|
|
|
154
144
|
// Go to page 2 first
|
|
155
145
|
act(() => {
|
|
@@ -169,7 +159,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
169
159
|
|
|
170
160
|
it('should navigate to first page', () => {
|
|
171
161
|
const pageSize = 2
|
|
172
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize})
|
|
162
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
173
163
|
|
|
174
164
|
// Go to last page first
|
|
175
165
|
act(() => {
|
|
@@ -189,7 +179,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
189
179
|
|
|
190
180
|
it('should navigate to last page', () => {
|
|
191
181
|
const pageSize = 2
|
|
192
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize})
|
|
182
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
193
183
|
|
|
194
184
|
act(() => {
|
|
195
185
|
result.current.lastPage()
|
|
@@ -201,7 +191,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
201
191
|
|
|
202
192
|
it('should navigate to specific page', () => {
|
|
203
193
|
const pageSize = 2
|
|
204
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize})
|
|
194
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
205
195
|
|
|
206
196
|
act(() => {
|
|
207
197
|
result.current.goToPage(2) // Go to page 2
|
|
@@ -226,7 +216,7 @@ describe('usePaginatedDocuments', () => {
|
|
|
226
216
|
|
|
227
217
|
it('should set page availability flags correctly', () => {
|
|
228
218
|
const pageSize = 2
|
|
229
|
-
const {result} = renderHook(() => usePaginatedDocuments({pageSize})
|
|
219
|
+
const {result} = renderHook(() => usePaginatedDocuments({pageSize}))
|
|
230
220
|
// On first page
|
|
231
221
|
expect(result.current.hasFirstPage).toBe(false)
|
|
232
222
|
expect(result.current.hasPreviousPage).toBe(false)
|
|
@@ -254,7 +244,6 @@ describe('usePaginatedDocuments', () => {
|
|
|
254
244
|
it('should reset current page when filter changes', () => {
|
|
255
245
|
const {result, rerender} = renderHook((props) => usePaginatedDocuments(props), {
|
|
256
246
|
initialProps: {pageSize: 2, filter: ''},
|
|
257
|
-
wrapper,
|
|
258
247
|
})
|
|
259
248
|
// Initially, current page should be 1
|
|
260
249
|
expect(result.current.currentPage).toBe(1)
|
|
@@ -270,15 +259,19 @@ describe('usePaginatedDocuments', () => {
|
|
|
270
259
|
})
|
|
271
260
|
|
|
272
261
|
it('should add projectId and dataset to document handles', () => {
|
|
273
|
-
const {result} = renderHook(() => usePaginatedDocuments({})
|
|
262
|
+
const {result} = renderHook(() => usePaginatedDocuments({}))
|
|
274
263
|
|
|
275
264
|
// Check that the first document handle has the projectId and dataset
|
|
276
|
-
expect(result.current.data[0].projectId).toBe('
|
|
277
|
-
expect(result.current.data[0].dataset).toBe('
|
|
265
|
+
expect((result.current.data[0].resource as DatasetResource).projectId).toBe('test')
|
|
266
|
+
expect((result.current.data[0].resource as DatasetResource).dataset).toBe('test')
|
|
278
267
|
|
|
279
268
|
// Verify all document handles have these properties
|
|
280
|
-
expect(
|
|
281
|
-
|
|
282
|
-
|
|
269
|
+
expect(
|
|
270
|
+
result.current.data.every(
|
|
271
|
+
(doc) =>
|
|
272
|
+
(doc.resource as DatasetResource).projectId === 'test' &&
|
|
273
|
+
(doc.resource as DatasetResource).dataset === 'test',
|
|
274
|
+
),
|
|
275
|
+
).toBe(true)
|
|
283
276
|
})
|
|
284
277
|
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {createGroqSearchFilter, type
|
|
1
|
+
import {createGroqSearchFilter, type QueryOptions} from '@sanity/sdk'
|
|
2
2
|
import {type SortOrderingItem} from '@sanity/types'
|
|
3
3
|
import {pick} from 'lodash-es'
|
|
4
4
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {type DocumentHandle, type ResourceHandle} from '../../config/handles'
|
|
7
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
7
8
|
import {useQuery} from '../query/useQuery'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -16,7 +17,10 @@ export interface PaginatedDocumentsOptions<
|
|
|
16
17
|
TDocumentType extends string = string,
|
|
17
18
|
TDataset extends string = string,
|
|
18
19
|
TProjectId extends string = string,
|
|
19
|
-
>
|
|
20
|
+
>
|
|
21
|
+
extends
|
|
22
|
+
ResourceHandle<TProjectId, TDataset>,
|
|
23
|
+
Pick<QueryOptions<TDocumentType, TDataset, TProjectId>, 'params'> {
|
|
20
24
|
documentType?: TDocumentType | TDocumentType[]
|
|
21
25
|
/**
|
|
22
26
|
* GROQ filter expression to apply to the query
|
|
@@ -137,18 +141,16 @@ export interface PaginatedDocumentsResponse<
|
|
|
137
141
|
* @returns An object containing the list of document handles, pagination details, and functions to navigate between pages
|
|
138
142
|
*
|
|
139
143
|
* @remarks
|
|
140
|
-
* - The returned document handles include
|
|
144
|
+
* - The returned document handles include resource information from the current Sanity instance
|
|
141
145
|
* - This makes them ready to use with document operations and other document hooks
|
|
142
|
-
* - The hook automatically uses the correct Sanity instance based on the
|
|
146
|
+
* - The hook automatically uses the correct Sanity instance based on the resource in the options
|
|
143
147
|
*
|
|
144
148
|
* @example Paginated list of documents with navigation
|
|
145
149
|
* ```tsx
|
|
146
150
|
* import {
|
|
147
151
|
* usePaginatedDocuments,
|
|
148
|
-
* createDatasetHandle,
|
|
149
|
-
* type DatasetHandle,
|
|
150
152
|
* type DocumentHandle,
|
|
151
|
-
* type
|
|
153
|
+
* type DocumentResource,
|
|
152
154
|
* useDocumentProjection
|
|
153
155
|
* } from '@sanity/sdk-react'
|
|
154
156
|
* import {Suspense} from 'react'
|
|
@@ -171,10 +173,10 @@ export interface PaginatedDocumentsResponse<
|
|
|
171
173
|
* // Define props for the list component
|
|
172
174
|
* interface PaginatedDocumentListProps {
|
|
173
175
|
* documentType: string
|
|
174
|
-
*
|
|
176
|
+
* resource?: DocumentResource
|
|
175
177
|
* }
|
|
176
178
|
*
|
|
177
|
-
* function PaginatedDocumentList({documentType,
|
|
179
|
+
* function PaginatedDocumentList({documentType, resource}: PaginatedDocumentListProps) {
|
|
178
180
|
* const {
|
|
179
181
|
* data,
|
|
180
182
|
* isPending,
|
|
@@ -185,7 +187,7 @@ export interface PaginatedDocumentsResponse<
|
|
|
185
187
|
* hasNextPage,
|
|
186
188
|
* hasPreviousPage
|
|
187
189
|
* } = usePaginatedDocuments({
|
|
188
|
-
*
|
|
190
|
+
* resource,
|
|
189
191
|
* documentType,
|
|
190
192
|
* pageSize: 10,
|
|
191
193
|
* orderings: [{field: '_createdAt', direction: 'desc'}],
|
|
@@ -217,8 +219,7 @@ export interface PaginatedDocumentsResponse<
|
|
|
217
219
|
* }
|
|
218
220
|
*
|
|
219
221
|
* // Usage:
|
|
220
|
-
* //
|
|
221
|
-
* // <PaginatedDocumentList dataset={myDatasetHandle} documentType="post" />
|
|
222
|
+
* // <PaginatedDocumentList resource={{projectId: 'p1', dataset: 'production'}} documentType="post" />
|
|
222
223
|
* ```
|
|
223
224
|
*/
|
|
224
225
|
export function usePaginatedDocuments<
|
|
@@ -232,15 +233,15 @@ export function usePaginatedDocuments<
|
|
|
232
233
|
params = {},
|
|
233
234
|
orderings,
|
|
234
235
|
search,
|
|
235
|
-
...
|
|
236
|
+
...rawOptions
|
|
236
237
|
}: PaginatedDocumentsOptions<TDocumentType, TDataset, TProjectId>): PaginatedDocumentsResponse<
|
|
237
238
|
TDocumentType,
|
|
238
239
|
TDataset,
|
|
239
240
|
TProjectId
|
|
240
241
|
> {
|
|
241
|
-
const
|
|
242
|
+
const options = useNormalizedResourceOptions(rawOptions)
|
|
242
243
|
const [pageIndex, setPageIndex] = useState(0)
|
|
243
|
-
const key = JSON.stringify({filter, search, params, orderings, pageSize})
|
|
244
|
+
const key = JSON.stringify({filter, search, params, orderings, pageSize, ...options})
|
|
244
245
|
// Reset the pageIndex to 0 whenever any query parameters (filter, search,
|
|
245
246
|
// params, orderings) or pageSize changes
|
|
246
247
|
useEffect(() => {
|
|
@@ -300,10 +301,8 @@ export function usePaginatedDocuments<
|
|
|
300
301
|
params: {
|
|
301
302
|
...params,
|
|
302
303
|
__types: documentTypes,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
...pick(options, 'projectId', 'dataset', 'perspective'),
|
|
306
|
-
},
|
|
304
|
+
// these are passed back to the user as part of each document handle
|
|
305
|
+
__handle: pick(options, ['resource', 'perspective']),
|
|
307
306
|
},
|
|
308
307
|
})
|
|
309
308
|
|
|
@@ -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,60 @@ 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 throw an error when used with a canvas resource', () => {
|
|
106
|
+
expect(() => {
|
|
107
|
+
renderHook(() => usePresence({resource: {canvasId: 'canvas123'}}), {
|
|
108
|
+
wrapper: ({children}) => (
|
|
109
|
+
<ResourceProvider
|
|
110
|
+
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
111
|
+
fallback={null}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</ResourceProvider>
|
|
115
|
+
),
|
|
116
|
+
})
|
|
117
|
+
}).toThrow('usePresence() does not support canvas resources')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should work with a dataset resource', () => {
|
|
121
|
+
const mockPresenceSource = {
|
|
122
|
+
getCurrent: vi.fn().mockReturnValue([]),
|
|
123
|
+
subscribe: vi.fn(() => () => {}),
|
|
124
|
+
observable: NEVER,
|
|
125
|
+
}
|
|
126
|
+
vi.mocked(getPresence).mockReturnValue(mockPresenceSource)
|
|
127
|
+
|
|
128
|
+
const {result, unmount} = renderHook(
|
|
129
|
+
() => usePresence({resource: {projectId: 'test-project', dataset: 'test-dataset'}}),
|
|
130
|
+
{
|
|
131
|
+
wrapper: ({children}) => (
|
|
132
|
+
<ResourceProvider
|
|
133
|
+
resource={{projectId: 'test-project', dataset: 'test-dataset'}}
|
|
134
|
+
fallback={null}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</ResourceProvider>
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
expect(result.current.locations).toEqual([])
|
|
143
|
+
unmount()
|
|
144
|
+
})
|
|
83
145
|
})
|