@sanity/sdk-react 2.5.0 → 2.7.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 +164 -19
- package/dist/index.d.ts +571 -26
- package/dist/index.js +149 -78
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/_exports/sdk-react.ts +2 -0
- package/src/components/SDKProvider.tsx +8 -3
- package/src/components/SanityApp.test.tsx +72 -2
- package/src/components/SanityApp.tsx +53 -10
- package/src/components/auth/AuthBoundary.tsx +5 -5
- package/src/context/ComlinkTokenRefresh.test.tsx +2 -2
- package/src/context/ComlinkTokenRefresh.tsx +3 -2
- package/src/context/SDKStudioContext.test.tsx +126 -0
- package/src/context/SDKStudioContext.ts +65 -0
- package/src/context/SourcesContext.tsx +7 -0
- package/src/context/renderSanityApp.test.tsx +355 -0
- package/src/context/renderSanityApp.tsx +48 -0
- package/src/hooks/agent/agentActions.ts +436 -21
- package/src/hooks/dashboard/useDispatchIntent.test.ts +26 -20
- package/src/hooks/dashboard/useDispatchIntent.ts +10 -11
- package/src/hooks/dashboard/utils/{getResourceIdFromDocumentHandle.test.ts → useResourceIdFromDocumentHandle.test.ts} +33 -60
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +46 -0
- package/src/hooks/document/useEditDocument.ts +3 -0
- package/src/hooks/documents/useDocuments.ts +3 -2
- package/src/hooks/helpers/useNormalizedSourceOptions.ts +85 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +1 -0
- package/src/hooks/projection/useDocumentProjection.ts +15 -4
- package/src/hooks/query/useQuery.ts +30 -11
- package/src/hooks/dashboard/types.ts +0 -12
- package/src/hooks/dashboard/utils/getResourceIdFromDocumentHandle.ts +0 -53
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
2
|
-
import {type FrameMessage} from '@sanity/sdk'
|
|
2
|
+
import {type DocumentHandle, type FrameMessage} from '@sanity/sdk'
|
|
3
3
|
import {useCallback} from 'react'
|
|
4
4
|
|
|
5
5
|
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
6
|
-
import {type
|
|
7
|
-
import {
|
|
6
|
+
import {type WithSourceNameSupport} from '../helpers/useNormalizedSourceOptions'
|
|
7
|
+
import {useResourceIdFromDocumentHandle} from './utils/useResourceIdFromDocumentHandle'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Message type for sending intents to the dashboard
|
|
@@ -42,7 +42,7 @@ interface DispatchIntent {
|
|
|
42
42
|
interface UseDispatchIntentParams {
|
|
43
43
|
action?: 'edit'
|
|
44
44
|
intentId?: string
|
|
45
|
-
documentHandle:
|
|
45
|
+
documentHandle: WithSourceNameSupport<DocumentHandle>
|
|
46
46
|
parameters?: Record<string, unknown>
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -104,14 +104,14 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
|
|
|
104
104
|
connectTo: SDK_CHANNEL_NAME,
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
+
const resource = useResourceIdFromDocumentHandle(documentHandle)
|
|
108
|
+
|
|
107
109
|
const dispatchIntent = useCallback(() => {
|
|
108
110
|
try {
|
|
109
111
|
if (!action && !intentId) {
|
|
110
112
|
throw new Error('useDispatchIntent: Either `action` or `intentId` must be provided.')
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
const {projectId, dataset, source} = documentHandle
|
|
114
|
-
|
|
115
115
|
if (action && intentId) {
|
|
116
116
|
// eslint-disable-next-line no-console -- warn if both action and intentId are provided
|
|
117
117
|
console.warn(
|
|
@@ -119,14 +119,13 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
|
|
|
119
119
|
)
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
// Validate that we have a resource ID (which is computed from source/sourceName or projectId+dataset)
|
|
123
|
+
if (!resource.id) {
|
|
123
124
|
throw new Error(
|
|
124
|
-
'useDispatchIntent: Either `source` or both `projectId` and `dataset` must be provided in documentHandle.',
|
|
125
|
+
'useDispatchIntent: Unable to determine resource. Either `source`, `sourceName`, or both `projectId` and `dataset` must be provided in documentHandle.',
|
|
125
126
|
)
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
const resource = getResourceIdFromDocumentHandle(documentHandle)
|
|
129
|
-
|
|
130
129
|
const message: IntentMessage = {
|
|
131
130
|
type: 'dashboard/v1/events/intents/dispatch-intent',
|
|
132
131
|
data: {
|
|
@@ -150,7 +149,7 @@ export function useDispatchIntent(params: UseDispatchIntentParams): DispatchInte
|
|
|
150
149
|
console.error('Failed to dispatch intent:', error)
|
|
151
150
|
throw error
|
|
152
151
|
}
|
|
153
|
-
}, [action, intentId, documentHandle, parameters, sendMessage])
|
|
152
|
+
}, [action, intentId, documentHandle, parameters, sendMessage, resource.id, resource.type])
|
|
154
153
|
|
|
155
154
|
return {
|
|
156
155
|
dispatchIntent,
|
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
canvasSource,
|
|
3
|
-
datasetSource,
|
|
4
|
-
type DocumentHandle,
|
|
5
|
-
type DocumentSource,
|
|
6
|
-
mediaLibrarySource,
|
|
7
|
-
} from '@sanity/sdk'
|
|
8
1
|
import {describe, expect, it} from 'vitest'
|
|
9
2
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
3
|
+
import {renderHook} from '../../../../test/test-utils'
|
|
4
|
+
import {useResourceIdFromDocumentHandle} from './useResourceIdFromDocumentHandle'
|
|
12
5
|
|
|
13
6
|
describe('getResourceIdFromDocumentHandle', () => {
|
|
14
7
|
describe('with traditional DocumentHandle (projectId/dataset)', () => {
|
|
15
8
|
it('should return resource ID from projectId and dataset', () => {
|
|
16
|
-
const documentHandle
|
|
9
|
+
const documentHandle = {
|
|
17
10
|
documentId: 'test-document-id',
|
|
18
11
|
documentType: 'test-document-type',
|
|
19
12
|
projectId: 'test-project-id',
|
|
20
13
|
dataset: 'test-dataset',
|
|
21
14
|
}
|
|
22
15
|
|
|
23
|
-
const result =
|
|
16
|
+
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
24
17
|
|
|
25
|
-
expect(result).toEqual({
|
|
18
|
+
expect(result.current).toEqual({
|
|
26
19
|
id: 'test-project-id.test-dataset',
|
|
27
20
|
type: undefined,
|
|
28
21
|
})
|
|
@@ -31,33 +24,33 @@ describe('getResourceIdFromDocumentHandle', () => {
|
|
|
31
24
|
|
|
32
25
|
describe('with DocumentHandleWithSource - media library', () => {
|
|
33
26
|
it('should return media library ID and resourceType when media library source is provided', () => {
|
|
34
|
-
const documentHandle
|
|
27
|
+
const documentHandle = {
|
|
35
28
|
documentId: 'test-asset-id',
|
|
36
29
|
documentType: 'sanity.asset',
|
|
37
|
-
|
|
38
|
-
}
|
|
30
|
+
sourceName: 'media-library',
|
|
31
|
+
} as const
|
|
39
32
|
|
|
40
|
-
const result =
|
|
33
|
+
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
41
34
|
|
|
42
|
-
expect(result).toEqual({
|
|
43
|
-
id: '
|
|
35
|
+
expect(result.current).toEqual({
|
|
36
|
+
id: 'media-library-id',
|
|
44
37
|
type: 'media-library',
|
|
45
38
|
})
|
|
46
39
|
})
|
|
47
40
|
|
|
48
41
|
it('should prioritize source over projectId/dataset when both are provided', () => {
|
|
49
|
-
const documentHandle
|
|
42
|
+
const documentHandle = {
|
|
50
43
|
documentId: 'test-asset-id',
|
|
51
44
|
documentType: 'sanity.asset',
|
|
52
45
|
projectId: 'test-project-id',
|
|
53
46
|
dataset: 'test-dataset',
|
|
54
|
-
|
|
47
|
+
sourceName: 'media-library',
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
const result =
|
|
50
|
+
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
58
51
|
|
|
59
|
-
expect(result).toEqual({
|
|
60
|
-
id: '
|
|
52
|
+
expect(result.current).toEqual({
|
|
53
|
+
id: 'media-library-id',
|
|
61
54
|
type: 'media-library',
|
|
62
55
|
})
|
|
63
56
|
})
|
|
@@ -65,16 +58,16 @@ describe('getResourceIdFromDocumentHandle', () => {
|
|
|
65
58
|
|
|
66
59
|
describe('with DocumentHandleWithSource - canvas', () => {
|
|
67
60
|
it('should return canvas ID and resourceType when canvas source is provided', () => {
|
|
68
|
-
const documentHandle
|
|
61
|
+
const documentHandle = {
|
|
69
62
|
documentId: 'test-canvas-document-id',
|
|
70
63
|
documentType: 'sanity.canvas.document',
|
|
71
|
-
|
|
64
|
+
sourceName: 'canvas',
|
|
72
65
|
}
|
|
73
66
|
|
|
74
|
-
const result =
|
|
67
|
+
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
75
68
|
|
|
76
|
-
expect(result).toEqual({
|
|
77
|
-
id: '
|
|
69
|
+
expect(result.current).toEqual({
|
|
70
|
+
id: 'canvas-id',
|
|
78
71
|
type: 'canvas',
|
|
79
72
|
})
|
|
80
73
|
})
|
|
@@ -82,32 +75,32 @@ describe('getResourceIdFromDocumentHandle', () => {
|
|
|
82
75
|
|
|
83
76
|
describe('with DocumentHandleWithSource - dataset source', () => {
|
|
84
77
|
it('should return dataset resource ID when dataset source is provided', () => {
|
|
85
|
-
const documentHandle
|
|
78
|
+
const documentHandle = {
|
|
86
79
|
documentId: 'test-document-id',
|
|
87
80
|
documentType: 'test-document-type',
|
|
88
|
-
|
|
81
|
+
sourceName: 'dataset',
|
|
89
82
|
}
|
|
90
83
|
|
|
91
|
-
const result =
|
|
84
|
+
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
92
85
|
|
|
93
|
-
expect(result).toEqual({
|
|
86
|
+
expect(result.current).toEqual({
|
|
94
87
|
id: 'source-project-id.source-dataset',
|
|
95
88
|
type: undefined,
|
|
96
89
|
})
|
|
97
90
|
})
|
|
98
91
|
|
|
99
92
|
it('should use dataset source over projectId/dataset when both are provided', () => {
|
|
100
|
-
const documentHandle
|
|
93
|
+
const documentHandle = {
|
|
101
94
|
documentId: 'test-document-id',
|
|
102
95
|
documentType: 'test-document-type',
|
|
103
96
|
projectId: 'test-project-id',
|
|
104
97
|
dataset: 'test-dataset',
|
|
105
|
-
|
|
98
|
+
sourceName: 'dataset',
|
|
106
99
|
}
|
|
107
100
|
|
|
108
|
-
const result =
|
|
101
|
+
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
109
102
|
|
|
110
|
-
expect(result).toEqual({
|
|
103
|
+
expect(result.current).toEqual({
|
|
111
104
|
id: 'source-project-id.source-dataset',
|
|
112
105
|
type: undefined,
|
|
113
106
|
})
|
|
@@ -116,37 +109,17 @@ describe('getResourceIdFromDocumentHandle', () => {
|
|
|
116
109
|
|
|
117
110
|
describe('edge cases', () => {
|
|
118
111
|
it('should handle DocumentHandleWithSource with undefined source', () => {
|
|
119
|
-
const documentHandle
|
|
120
|
-
documentId: 'test-document-id',
|
|
121
|
-
documentType: 'test-document-type',
|
|
122
|
-
projectId: 'test-project-id',
|
|
123
|
-
dataset: 'test-dataset',
|
|
124
|
-
source: undefined,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const result = getResourceIdFromDocumentHandle(documentHandle)
|
|
128
|
-
|
|
129
|
-
expect(result).toEqual({
|
|
130
|
-
id: 'test-project-id.test-dataset',
|
|
131
|
-
type: undefined,
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('should fall back to projectId/dataset when source is not recognized', () => {
|
|
136
|
-
const documentHandle: DocumentHandleWithSource = {
|
|
112
|
+
const documentHandle = {
|
|
137
113
|
documentId: 'test-document-id',
|
|
138
114
|
documentType: 'test-document-type',
|
|
139
115
|
projectId: 'test-project-id',
|
|
140
116
|
dataset: 'test-dataset',
|
|
141
|
-
|
|
142
|
-
__sanity_internal_sourceId: 'unknown-format',
|
|
143
|
-
} as unknown as DocumentSource,
|
|
117
|
+
sourceName: undefined,
|
|
144
118
|
}
|
|
145
119
|
|
|
146
|
-
const result =
|
|
120
|
+
const {result} = renderHook(() => useResourceIdFromDocumentHandle(documentHandle))
|
|
147
121
|
|
|
148
|
-
|
|
149
|
-
expect(result).toEqual({
|
|
122
|
+
expect(result.current).toEqual({
|
|
150
123
|
id: 'test-project-id.test-dataset',
|
|
151
124
|
type: undefined,
|
|
152
125
|
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type DocumentHandle,
|
|
3
|
+
isCanvasSource,
|
|
4
|
+
isDatasetSource,
|
|
5
|
+
isMediaLibrarySource,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
|
|
8
|
+
import {useNormalizedSourceOptions} from '../../helpers/useNormalizedSourceOptions'
|
|
9
|
+
|
|
10
|
+
interface DashboardMessageResource {
|
|
11
|
+
id: string
|
|
12
|
+
type?: 'media-library' | 'canvas'
|
|
13
|
+
}
|
|
14
|
+
/** Currently only used for dispatching intents to the dashboard,
|
|
15
|
+
* but could easily be extended to other dashboard hooks
|
|
16
|
+
* @beta
|
|
17
|
+
*/
|
|
18
|
+
export function useResourceIdFromDocumentHandle(
|
|
19
|
+
documentHandle: DocumentHandle,
|
|
20
|
+
): DashboardMessageResource {
|
|
21
|
+
const options = useNormalizedSourceOptions(documentHandle)
|
|
22
|
+
const {projectId, dataset, source} = options
|
|
23
|
+
let resourceId: string = ''
|
|
24
|
+
let resourceType: 'media-library' | 'canvas' | undefined
|
|
25
|
+
if (projectId && dataset) {
|
|
26
|
+
resourceId = `${projectId}.${dataset}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (source) {
|
|
30
|
+
if (isDatasetSource(source)) {
|
|
31
|
+
resourceId = `${source.projectId}.${source.dataset}`
|
|
32
|
+
resourceType = undefined
|
|
33
|
+
} else if (isMediaLibrarySource(source)) {
|
|
34
|
+
resourceId = source.mediaLibraryId
|
|
35
|
+
resourceType = 'media-library'
|
|
36
|
+
} else if (isCanvasSource(source)) {
|
|
37
|
+
resourceId = source.canvasId
|
|
38
|
+
resourceType = 'canvas'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: resourceId,
|
|
44
|
+
type: resourceType,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -100,6 +100,9 @@ export function useEditDocument<TData>(
|
|
|
100
100
|
* 3. **Explicit Type (Full Document):** Edit the entire document with a manually specified type.
|
|
101
101
|
* 4. **Explicit Type (Specific Path):** Edit a specific field with a manually specified type.
|
|
102
102
|
*
|
|
103
|
+
* **LiveEdit Documents:**
|
|
104
|
+
* For documents using {@link DocumentHandle.liveEdit | liveEdit mode} (set via `liveEdit: true` in the document handle), edits are applied directly to the published document without creating a draft.
|
|
105
|
+
*
|
|
103
106
|
* This hook relies on the document state being loaded. If the document is not yet available
|
|
104
107
|
* (e.g., during initial load), the component using this hook will suspend.
|
|
105
108
|
*
|
|
@@ -23,8 +23,8 @@ export interface DocumentsOptions<
|
|
|
23
23
|
TDocumentType extends string = string,
|
|
24
24
|
TDataset extends string = string,
|
|
25
25
|
TProjectId extends string = string,
|
|
26
|
-
>
|
|
27
|
-
|
|
26
|
+
>
|
|
27
|
+
extends DatasetHandle<TDataset, TProjectId>, Pick<QueryOptions, 'perspective' | 'params'> {
|
|
28
28
|
/**
|
|
29
29
|
* Filter documents by their `_type`. Can be a single type or an array of types.
|
|
30
30
|
*/
|
|
@@ -39,6 +39,7 @@ export interface DocumentsOptions<
|
|
|
39
39
|
batchSize?: number
|
|
40
40
|
/**
|
|
41
41
|
* Sorting configuration for the results
|
|
42
|
+
* @beta
|
|
42
43
|
*/
|
|
43
44
|
orderings?: SortOrderingItem[]
|
|
44
45
|
/**
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {type DocumentSource} from '@sanity/sdk'
|
|
2
|
+
import {useContext} from 'react'
|
|
3
|
+
|
|
4
|
+
import {SourcesContext} from '../../context/SourcesContext'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Adds React hook support (sourceName resolution) to core types.
|
|
8
|
+
* This wrapper allows hooks to accept `sourceName` as a convenience,
|
|
9
|
+
* which is then resolved to a `DocumentSource` at the React layer.
|
|
10
|
+
* For now, we are trying to avoid source name resolution in core --
|
|
11
|
+
* functions having sources explicitly passed will reduce complexity.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam T - The core type to extend (must have optional `source` field)
|
|
14
|
+
* @beta
|
|
15
|
+
*/
|
|
16
|
+
export type WithSourceNameSupport<T extends {source?: DocumentSource}> = T & {
|
|
17
|
+
/**
|
|
18
|
+
* Optional name of a source to resolve from context.
|
|
19
|
+
* If provided, will be resolved to a `DocumentSource` via `SourcesContext`.
|
|
20
|
+
* @beta
|
|
21
|
+
*/
|
|
22
|
+
sourceName?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes hook options by resolving `sourceName` to a `DocumentSource`.
|
|
27
|
+
* This hook ensures that options passed to core layer functions only contain
|
|
28
|
+
* `source` (never `sourceName`), preventing duplicate cache keys and maintaining
|
|
29
|
+
* clean separation between React and core layers.
|
|
30
|
+
*
|
|
31
|
+
* @typeParam T - The options type (must include optional source field)
|
|
32
|
+
* @param options - Hook options that may include `sourceName` and/or `source`
|
|
33
|
+
* @returns Normalized options with `sourceName` removed and `source` resolved
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Resolution priority:
|
|
37
|
+
* 1. If `sourceName` is provided, resolves it via `SourcesContext` and uses that
|
|
38
|
+
* 2. Otherwise, uses the inline `source` if provided
|
|
39
|
+
* 3. If neither is provided, returns options without a source field
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```tsx
|
|
43
|
+
* function useQuery(options: WithSourceNameSupport<QueryOptions>) {
|
|
44
|
+
* const instance = useSanityInstance(options)
|
|
45
|
+
* const normalized = useNormalizedOptions(options)
|
|
46
|
+
* // normalized now has source but never sourceName
|
|
47
|
+
* const queryKey = getQueryKey(normalized)
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @beta
|
|
52
|
+
*/
|
|
53
|
+
export function useNormalizedSourceOptions<
|
|
54
|
+
T extends {source?: DocumentSource; sourceName?: string},
|
|
55
|
+
>(options: T): Omit<T, 'sourceName'> {
|
|
56
|
+
const {sourceName, ...rest} = options
|
|
57
|
+
if (sourceName && Object.hasOwn(options, 'source')) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Source name ${JSON.stringify(sourceName)} and source ${JSON.stringify(options.source)} cannot be used together.`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Resolve sourceName to source via context
|
|
64
|
+
const sources = useContext(SourcesContext)
|
|
65
|
+
let resolvedSource: DocumentSource | undefined
|
|
66
|
+
|
|
67
|
+
if (options.source) {
|
|
68
|
+
resolvedSource = options.source
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (sourceName && !Object.hasOwn(sources, sourceName)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`There's no source named ${JSON.stringify(sourceName)} in context. Please use <SourceProvider>.`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sourceName && sources[sourceName]) {
|
|
78
|
+
resolvedSource = sources[sourceName]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
...rest,
|
|
83
|
+
source: resolvedSource,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -4,6 +4,10 @@ import {useCallback, useMemo, useSyncExternalStore} from 'react'
|
|
|
4
4
|
import {distinctUntilChanged, EMPTY, Observable, startWith, switchMap} from 'rxjs'
|
|
5
5
|
|
|
6
6
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
+
import {
|
|
8
|
+
useNormalizedSourceOptions,
|
|
9
|
+
type WithSourceNameSupport,
|
|
10
|
+
} from '../helpers/useNormalizedSourceOptions'
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* @public
|
|
@@ -14,7 +18,7 @@ export interface useDocumentProjectionOptions<
|
|
|
14
18
|
TDocumentType extends string = string,
|
|
15
19
|
TDataset extends string = string,
|
|
16
20
|
TProjectId extends string = string,
|
|
17
|
-
> extends DocumentHandle<TDocumentType, TDataset, TProjectId
|
|
21
|
+
> extends WithSourceNameSupport<DocumentHandle<TDocumentType, TDataset, TProjectId>> {
|
|
18
22
|
/** The GROQ projection string */
|
|
19
23
|
projection: TProjection
|
|
20
24
|
/** Optional parameters for the projection query */
|
|
@@ -183,15 +187,22 @@ export function useDocumentProjection<TData extends object>({
|
|
|
183
187
|
// even if the string reference changes (e.g., from inline template literals)
|
|
184
188
|
const normalizedProjection = useMemo(() => projection.trim(), [projection])
|
|
185
189
|
|
|
190
|
+
// Normalize options: resolve sourceName to source and strip sourceName
|
|
191
|
+
const normalizedDocHandle = useNormalizedSourceOptions(docHandle)
|
|
192
|
+
|
|
186
193
|
// Memoize stateSource based on normalized projection and docHandle properties
|
|
187
194
|
// This prevents creating a new StateSource on every render when projection content is the same
|
|
188
195
|
const stateSource = useMemo(
|
|
189
|
-
() =>
|
|
190
|
-
|
|
196
|
+
() =>
|
|
197
|
+
getProjectionState<TData>(instance, {
|
|
198
|
+
...normalizedDocHandle,
|
|
199
|
+
projection: normalizedProjection,
|
|
200
|
+
}),
|
|
201
|
+
[instance, normalizedDocHandle, normalizedProjection],
|
|
191
202
|
)
|
|
192
203
|
|
|
193
204
|
if (stateSource.getCurrent()?.data === null) {
|
|
194
|
-
throw resolveProjection(instance, {...
|
|
205
|
+
throw resolveProjection(instance, {...normalizedDocHandle, projection: normalizedProjection})
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
// Create subscribe function for useSyncExternalStore
|
|
@@ -9,6 +9,19 @@ import {type SanityQueryResult} from 'groq'
|
|
|
9
9
|
import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
|
|
10
10
|
|
|
11
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
import {
|
|
13
|
+
useNormalizedSourceOptions,
|
|
14
|
+
type WithSourceNameSupport,
|
|
15
|
+
} from '../helpers/useNormalizedSourceOptions'
|
|
16
|
+
/**
|
|
17
|
+
* Hook options for useQuery, supporting both direct source and sourceName.
|
|
18
|
+
* @beta
|
|
19
|
+
*/
|
|
20
|
+
type UseQueryOptions<
|
|
21
|
+
TQuery extends string = string,
|
|
22
|
+
TDataset extends string = string,
|
|
23
|
+
TProjectId extends string = string,
|
|
24
|
+
> = WithSourceNameSupport<QueryOptions<TQuery, TDataset, TProjectId>>
|
|
12
25
|
|
|
13
26
|
// Overload 1: Inferred Type (using Typegen)
|
|
14
27
|
/**
|
|
@@ -71,7 +84,7 @@ export function useQuery<
|
|
|
71
84
|
TDataset extends string = string,
|
|
72
85
|
TProjectId extends string = string,
|
|
73
86
|
>(
|
|
74
|
-
options:
|
|
87
|
+
options: UseQueryOptions<TQuery, TDataset, TProjectId>,
|
|
75
88
|
): {
|
|
76
89
|
/** The query result, typed based on the GROQ query string */
|
|
77
90
|
data: SanityQueryResult<TQuery, `${TProjectId}.${TDataset}`>
|
|
@@ -108,7 +121,7 @@ export function useQuery<
|
|
|
108
121
|
* }
|
|
109
122
|
* ```
|
|
110
123
|
*/
|
|
111
|
-
export function useQuery<TData>(options: QueryOptions): {
|
|
124
|
+
export function useQuery<TData>(options: WithSourceNameSupport<QueryOptions>): {
|
|
112
125
|
/** The query result, cast to the provided type TData */
|
|
113
126
|
data: TData
|
|
114
127
|
/** True if another query is resolving in the background (suspense handles the initial loading state) */
|
|
@@ -133,19 +146,23 @@ export function useQuery<TData>(options: QueryOptions): {
|
|
|
133
146
|
*
|
|
134
147
|
* @category GROQ
|
|
135
148
|
*/
|
|
136
|
-
export function useQuery(options: QueryOptions): {
|
|
149
|
+
export function useQuery(options: WithSourceNameSupport<QueryOptions>): {
|
|
150
|
+
data: unknown
|
|
151
|
+
isPending: boolean
|
|
152
|
+
} {
|
|
137
153
|
// Implementation returns unknown, overloads define specifics
|
|
138
154
|
const instance = useSanityInstance(options)
|
|
139
155
|
|
|
156
|
+
// Normalize options: resolve sourceName to source and strip sourceName
|
|
157
|
+
const normalized = useNormalizedSourceOptions(options)
|
|
158
|
+
|
|
140
159
|
// Use React's useTransition to avoid UI jank when queries change
|
|
141
160
|
const [isPending, startTransition] = useTransition()
|
|
142
161
|
|
|
143
|
-
// Get the unique key for this query and its options
|
|
144
|
-
const queryKey = getQueryKey(
|
|
162
|
+
// Get the unique key for this query and its options (using normalized options)
|
|
163
|
+
const queryKey = getQueryKey(normalized)
|
|
145
164
|
// Use a deferred state to avoid immediate re-renders when the query changes
|
|
146
165
|
const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
|
|
147
|
-
// Parse the deferred query key back into a query and options
|
|
148
|
-
const deferred = useMemo(() => parseQueryKey(deferredQueryKey), [deferredQueryKey])
|
|
149
166
|
|
|
150
167
|
// Create an AbortController to cancel in-flight requests when needed
|
|
151
168
|
const ref = useRef<AbortController>(new AbortController())
|
|
@@ -166,10 +183,11 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
|
|
|
166
183
|
}, [deferredQueryKey, queryKey])
|
|
167
184
|
|
|
168
185
|
// Get the state source for this query from the query store
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
// Memoize the options object by depending on the stable string key instead of the parsed object
|
|
187
|
+
const {getCurrent, subscribe} = useMemo(() => {
|
|
188
|
+
const deferred = parseQueryKey(deferredQueryKey)
|
|
189
|
+
return getQueryState(instance, deferred)
|
|
190
|
+
}, [instance, deferredQueryKey])
|
|
173
191
|
|
|
174
192
|
// If data isn't available yet, suspend rendering
|
|
175
193
|
if (getCurrent() === undefined) {
|
|
@@ -182,6 +200,7 @@ export function useQuery(options: QueryOptions): {data: unknown; isPending: bool
|
|
|
182
200
|
// the captured signal remains unchanged for this suspended render.
|
|
183
201
|
// Thus, the promise thrown here uses a stable abort signal, ensuring correct behavior.
|
|
184
202
|
const currentSignal = ref.current.signal
|
|
203
|
+
const deferred = parseQueryKey(deferredQueryKey)
|
|
185
204
|
|
|
186
205
|
throw resolveQuery(instance, {...deferred, signal: currentSignal})
|
|
187
206
|
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import {type DocumentHandle, type DocumentSource} from '@sanity/sdk'
|
|
2
|
-
/**
|
|
3
|
-
* Document handle that optionally includes a source (e.g., media library source)
|
|
4
|
-
* or projectId and dataset for traditional dataset sources
|
|
5
|
-
* (but now marked optional since it's valid to just use a source)
|
|
6
|
-
* @beta
|
|
7
|
-
*/
|
|
8
|
-
export interface DocumentHandleWithSource extends Omit<DocumentHandle, 'projectId' | 'dataset'> {
|
|
9
|
-
source?: DocumentSource
|
|
10
|
-
projectId?: string
|
|
11
|
-
dataset?: string
|
|
12
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import {type DocumentHandle, type DocumentSource} from '@sanity/sdk'
|
|
2
|
-
|
|
3
|
-
import {type DocumentHandleWithSource} from '../types'
|
|
4
|
-
|
|
5
|
-
// Internal constant for accessing source ID
|
|
6
|
-
const SOURCE_ID = '__sanity_internal_sourceId' as const
|
|
7
|
-
|
|
8
|
-
interface DashboardMessageResource {
|
|
9
|
-
id: string
|
|
10
|
-
type?: 'media-library' | 'canvas'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const isDocumentHandleWithSource = (
|
|
14
|
-
documentHandle: DocumentHandle | DocumentHandleWithSource,
|
|
15
|
-
): documentHandle is DocumentHandleWithSource => {
|
|
16
|
-
return 'source' in documentHandle
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Currently only used for dispatching intents to the dashboard,
|
|
20
|
-
* but could easily be extended to other dashboard hooks
|
|
21
|
-
* @beta
|
|
22
|
-
*/
|
|
23
|
-
export function getResourceIdFromDocumentHandle(
|
|
24
|
-
documentHandle: DocumentHandle | DocumentHandleWithSource,
|
|
25
|
-
): DashboardMessageResource {
|
|
26
|
-
let source: DocumentSource | undefined
|
|
27
|
-
|
|
28
|
-
const {projectId, dataset} = documentHandle
|
|
29
|
-
if (isDocumentHandleWithSource(documentHandle)) {
|
|
30
|
-
source = documentHandle.source
|
|
31
|
-
}
|
|
32
|
-
let resourceId: string = projectId + '.' + dataset
|
|
33
|
-
let resourceType: 'media-library' | 'canvas' | undefined
|
|
34
|
-
|
|
35
|
-
if (source) {
|
|
36
|
-
const sourceId = (source as Record<string, unknown>)[SOURCE_ID]
|
|
37
|
-
if (Array.isArray(sourceId)) {
|
|
38
|
-
if (sourceId[0] === 'media-library' || sourceId[0] === 'canvas') {
|
|
39
|
-
resourceType = sourceId[0] as 'media-library' | 'canvas'
|
|
40
|
-
resourceId = sourceId[1] as string
|
|
41
|
-
}
|
|
42
|
-
} else if (sourceId && typeof sourceId === 'object' && 'projectId' in sourceId) {
|
|
43
|
-
const datasetSource = sourceId as {projectId: string; dataset: string}
|
|
44
|
-
// don't create type since it's ambiguous for project / dataset docs
|
|
45
|
-
resourceId = `${datasetSource.projectId}.${datasetSource.dataset}`
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
id: resourceId,
|
|
51
|
-
type: resourceType,
|
|
52
|
-
}
|
|
53
|
-
}
|