@sanity/sdk-react 0.0.0-rc.6 → 0.0.1
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 +5 -57
- package/dist/index.d.ts +1000 -438
- package/dist/index.js +324 -258
- package/dist/index.js.map +1 -1
- package/package.json +17 -16
- package/src/_exports/sdk-react.ts +4 -1
- package/src/components/SDKProvider.tsx +6 -1
- package/src/components/SanityApp.test.tsx +29 -47
- package/src/components/SanityApp.tsx +12 -11
- package/src/components/auth/AuthBoundary.test.tsx +177 -7
- package/src/components/auth/AuthBoundary.tsx +32 -2
- package/src/components/auth/ConfigurationError.ts +22 -0
- package/src/components/auth/LoginError.tsx +9 -3
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
- package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
- package/src/hooks/client/useClient.ts +3 -3
- package/src/hooks/comlink/useManageFavorite.test.ts +276 -27
- package/src/hooks/comlink/useManageFavorite.ts +102 -51
- package/src/hooks/comlink/useWindowConnection.ts +3 -2
- package/src/hooks/document/useApplyDocumentActions.ts +105 -31
- package/src/hooks/document/useDocument.test.ts +41 -4
- package/src/hooks/document/useDocument.ts +198 -114
- package/src/hooks/document/useDocumentEvent.test.ts +5 -5
- package/src/hooks/document/useDocumentEvent.ts +67 -23
- package/src/hooks/document/useDocumentPermissions.ts +47 -8
- package/src/hooks/document/useDocumentSyncStatus.test.ts +12 -5
- package/src/hooks/document/useDocumentSyncStatus.ts +41 -14
- package/src/hooks/document/useEditDocument.test.ts +24 -6
- package/src/hooks/document/useEditDocument.ts +238 -133
- package/src/hooks/documents/useDocuments.test.tsx +1 -1
- package/src/hooks/documents/useDocuments.ts +153 -44
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +1 -1
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +120 -47
- package/src/hooks/projection/useProjection.ts +134 -46
- package/src/hooks/query/useQuery.test.tsx +4 -4
- package/src/hooks/query/useQuery.ts +115 -43
- package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
- package/src/hooks/releases/useActiveReleases.ts +39 -0
- package/src/hooks/releases/usePerspective.test.tsx +120 -0
- package/src/hooks/releases/usePerspective.ts +50 -0
|
@@ -1,128 +1,212 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
getDocumentState,
|
|
4
|
-
type JsonMatch,
|
|
5
|
-
type JsonMatchPath,
|
|
6
|
-
resolveDocument,
|
|
7
|
-
} from '@sanity/sdk'
|
|
8
|
-
import {type SanityDocument} from '@sanity/types'
|
|
1
|
+
import {type DocumentOptions, getDocumentState, type JsonMatch, resolveDocument} from '@sanity/sdk'
|
|
2
|
+
import {type SanityDocumentResult} from 'groq'
|
|
9
3
|
import {identity} from 'rxjs'
|
|
10
4
|
|
|
11
5
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
6
|
+
// used in an `{@link useProjection}` and `{@link useQuery}`
|
|
7
|
+
// eslint-disable-next-line import/consistent-type-specifier-style, unused-imports/no-unused-imports
|
|
8
|
+
import type {useProjection} from '../projection/useProjection'
|
|
9
|
+
// eslint-disable-next-line import/consistent-type-specifier-style, unused-imports/no-unused-imports
|
|
10
|
+
import type {useQuery} from '../query/useQuery'
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* @category Documents
|
|
19
|
-
* @param doc - The document to read state from, specified as a DocumentHandle
|
|
20
|
-
* @param path - The path to the nested value to read from
|
|
21
|
-
* @returns The value at the specified path
|
|
22
|
-
* @example
|
|
23
|
-
* ```tsx
|
|
24
|
-
* import {useDocument} from '@sanity/sdk-react'
|
|
25
|
-
*
|
|
26
|
-
* const documentHandle = {
|
|
27
|
-
* documentId: 'order-123',
|
|
28
|
-
* documentType: 'order',
|
|
29
|
-
* projectId: 'abc123',
|
|
30
|
-
* dataset: 'production'
|
|
31
|
-
* }
|
|
32
|
-
*
|
|
33
|
-
* function OrderLink() {
|
|
34
|
-
* const title = useDocument(documentHandle, 'title')
|
|
35
|
-
* const id = useDocument(documentHandle, '_id')
|
|
36
|
-
*
|
|
37
|
-
* return (
|
|
38
|
-
* <a href={`/order/${id}`}>Order {title} today!</a>
|
|
39
|
-
* )
|
|
40
|
-
* }
|
|
41
|
-
* ```
|
|
42
|
-
*
|
|
43
|
-
*/
|
|
44
|
-
export function useDocument<
|
|
45
|
-
TDocument extends SanityDocument,
|
|
46
|
-
TPath extends JsonMatchPath<TDocument>,
|
|
47
|
-
>(doc: DocumentHandle<TDocument>, path: TPath): JsonMatch<TDocument, TPath> | undefined
|
|
12
|
+
interface UseDocument {
|
|
13
|
+
/** @internal */
|
|
14
|
+
<TDocumentType extends string, TDataset extends string, TProjectId extends string = string>(
|
|
15
|
+
options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
|
|
16
|
+
): SanityDocumentResult<TDocumentType, TDataset, TProjectId> | null
|
|
48
17
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
18
|
+
/** @internal */
|
|
19
|
+
<
|
|
20
|
+
TPath extends string,
|
|
21
|
+
TDocumentType extends string,
|
|
22
|
+
TDataset extends string = string,
|
|
23
|
+
TProjectId extends string = string,
|
|
24
|
+
>(
|
|
25
|
+
options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
|
|
26
|
+
): JsonMatch<SanityDocumentResult<TDocumentType, TDataset, TProjectId>, TPath> | undefined
|
|
27
|
+
|
|
28
|
+
/** @internal */
|
|
29
|
+
<TData>(options: DocumentOptions<undefined>): TData | null
|
|
30
|
+
/** @internal */
|
|
31
|
+
<TData>(options: DocumentOptions<string>): TData | undefined
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ## useDocument via Type Inference (Recommended)
|
|
35
|
+
*
|
|
36
|
+
* @beta
|
|
37
|
+
*
|
|
38
|
+
* The preferred way to use this hook when working with Sanity Typegen.
|
|
39
|
+
*
|
|
40
|
+
* Features:
|
|
41
|
+
* - Automatically infers document types from your schema
|
|
42
|
+
* - Provides type-safe access to documents and nested fields
|
|
43
|
+
* - Supports project/dataset-specific type inference
|
|
44
|
+
* - Works seamlessly with Typegen-generated types
|
|
45
|
+
*
|
|
46
|
+
* This hook will suspend while the document data is being fetched and loaded.
|
|
47
|
+
*
|
|
48
|
+
* When fetching a full document:
|
|
49
|
+
* - Returns the complete document object if it exists
|
|
50
|
+
* - Returns `null` if the document doesn't exist
|
|
51
|
+
*
|
|
52
|
+
* When fetching with a path:
|
|
53
|
+
* - Returns the value at the specified path if both the document and path exist
|
|
54
|
+
* - Returns `undefined` if either the document doesn't exist or the path doesn't exist in the document
|
|
55
|
+
*
|
|
56
|
+
* @category Documents
|
|
57
|
+
* @param options - Configuration including `documentId`, `documentType`, and optionally:
|
|
58
|
+
* - `path`: To select a nested value (returns typed value at path)
|
|
59
|
+
* - `projectId`/`dataset`: For multi-project/dataset setups
|
|
60
|
+
* @returns The document state (or nested value if path provided).
|
|
61
|
+
*
|
|
62
|
+
* @example Basic document fetch
|
|
63
|
+
* ```tsx
|
|
64
|
+
* import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
|
|
65
|
+
*
|
|
66
|
+
* interface ProductViewProps {
|
|
67
|
+
* doc: DocumentHandle<'product'> // Typegen infers product type
|
|
68
|
+
* }
|
|
69
|
+
*
|
|
70
|
+
* function ProductView({doc}: ProductViewProps) {
|
|
71
|
+
* const product = useDocument({...doc}) // Fully typed product
|
|
72
|
+
* return <h1>{product.title ?? 'Untitled'}</h1>
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example Fetching a specific field
|
|
77
|
+
* ```tsx
|
|
78
|
+
* import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
|
|
79
|
+
*
|
|
80
|
+
* interface ProductTitleProps {
|
|
81
|
+
* doc: DocumentHandle<'product'>
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* function ProductTitle({doc}: ProductTitleProps) {
|
|
85
|
+
* const title = useDocument({
|
|
86
|
+
* ...doc,
|
|
87
|
+
* path: 'title' // Returns just the title field
|
|
88
|
+
* })
|
|
89
|
+
* return <h1>{title ?? 'Untitled'}</h1>
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @inlineType DocumentOptions
|
|
94
|
+
*/
|
|
95
|
+
<
|
|
96
|
+
TPath extends string | undefined = undefined,
|
|
97
|
+
TDocumentType extends string = string,
|
|
98
|
+
TDataset extends string = string,
|
|
99
|
+
TProjectId extends string = string,
|
|
100
|
+
>(
|
|
101
|
+
options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
|
|
102
|
+
): TPath extends string
|
|
103
|
+
? JsonMatch<SanityDocumentResult<TDocumentType, TDataset, TProjectId>, TPath> | undefined
|
|
104
|
+
: SanityDocumentResult<TDocumentType, TDataset, TProjectId> | null
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @beta
|
|
108
|
+
*
|
|
109
|
+
* ## useDocument via Explicit Types
|
|
110
|
+
*
|
|
111
|
+
* Use this version when:
|
|
112
|
+
* - You're not using Sanity Typegen
|
|
113
|
+
* - You need to manually specify document types
|
|
114
|
+
* - You're working with dynamic document types
|
|
115
|
+
*
|
|
116
|
+
* Key differences from Typegen version:
|
|
117
|
+
* - Requires manual type specification via `TData`
|
|
118
|
+
* - Returns `TData | null` for full documents
|
|
119
|
+
* - Returns `TData | undefined` for nested values
|
|
120
|
+
*
|
|
121
|
+
* This hook will suspend while the document data is being fetched.
|
|
122
|
+
*
|
|
123
|
+
* @typeParam TData - The explicit type for the document or field
|
|
124
|
+
* @typeParam TPath - Optional path to a nested value
|
|
125
|
+
* @param options - Configuration including `documentId` and optionally:
|
|
126
|
+
* - `path`: To select a nested value
|
|
127
|
+
* - `projectId`/`dataset`: For multi-project/dataset setups
|
|
128
|
+
* @returns The document state (or nested value if path provided)
|
|
129
|
+
*
|
|
130
|
+
* @example Basic document fetch with explicit type
|
|
131
|
+
* ```tsx
|
|
132
|
+
* import {useDocument, type DocumentHandle, type SanityDocument} from '@sanity/sdk-react'
|
|
133
|
+
*
|
|
134
|
+
* interface Book extends SanityDocument {
|
|
135
|
+
* _type: 'book'
|
|
136
|
+
* title: string
|
|
137
|
+
* author: string
|
|
138
|
+
* }
|
|
139
|
+
*
|
|
140
|
+
* interface BookViewProps {
|
|
141
|
+
* doc: DocumentHandle
|
|
142
|
+
* }
|
|
143
|
+
*
|
|
144
|
+
* function BookView({doc}: BookViewProps) {
|
|
145
|
+
* const book = useDocument<Book>({...doc})
|
|
146
|
+
* return <h1>{book?.title ?? 'Untitled'} by {book?.author ?? 'Unknown'}</h1>
|
|
147
|
+
* }
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* @example Fetching a specific field with explicit type
|
|
151
|
+
* ```tsx
|
|
152
|
+
* import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
|
|
153
|
+
*
|
|
154
|
+
* interface BookTitleProps {
|
|
155
|
+
* doc: DocumentHandle
|
|
156
|
+
* }
|
|
157
|
+
*
|
|
158
|
+
* function BookTitle({doc}: BookTitleProps) {
|
|
159
|
+
* const title = useDocument<string>({...doc, path: 'title'})
|
|
160
|
+
* return <h1>{title ?? 'Untitled'}</h1>
|
|
161
|
+
* }
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* @inlineType DocumentOptions
|
|
165
|
+
*/
|
|
166
|
+
<TData, TPath extends string>(
|
|
167
|
+
options: DocumentOptions<TPath>,
|
|
168
|
+
): TPath extends string ? TData | undefined : TData | null
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @internal
|
|
172
|
+
*/
|
|
173
|
+
(options: DocumentOptions): unknown
|
|
174
|
+
}
|
|
98
175
|
|
|
99
176
|
/**
|
|
100
177
|
* @beta
|
|
101
178
|
* Reads and subscribes to a document's realtime state, incorporating both local and remote changes.
|
|
102
|
-
*
|
|
103
|
-
*
|
|
179
|
+
*
|
|
180
|
+
* This hook comes in two main flavors to suit your needs:
|
|
181
|
+
*
|
|
182
|
+
* 1. **[Type Inference](#usedocument-via-type-inference-recommended)** (Recommended) - Automatically gets types from your Sanity schema
|
|
183
|
+
* 2. **[Explicit Types](#usedocument-via-explicit-types)** - Manually specify types when needed
|
|
104
184
|
*
|
|
105
185
|
* @remarks
|
|
106
|
-
* `useDocument` is
|
|
107
|
-
*
|
|
108
|
-
* or realtime editing interface where local changes need to be reflected immediately.
|
|
186
|
+
* `useDocument` is ideal for realtime editing interfaces where you need immediate feedback on changes.
|
|
187
|
+
* However, it can be resource-intensive since it maintains a realtime connection.
|
|
109
188
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
189
|
+
* For simpler cases where:
|
|
190
|
+
* - You only need to display content
|
|
191
|
+
* - Realtime updates aren't critical
|
|
192
|
+
* - You want better performance
|
|
113
193
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
194
|
+
* …consider using {@link useProjection} or {@link useQuery} instead. These hooks are more efficient
|
|
195
|
+
* for read-heavy applications.
|
|
196
|
+
*
|
|
197
|
+
* @function
|
|
118
198
|
*/
|
|
119
|
-
export
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
199
|
+
export const useDocument = createStateSourceHook({
|
|
200
|
+
// Pass options directly to getDocumentState
|
|
201
|
+
getState: (instance, options: DocumentOptions<string | undefined>) =>
|
|
202
|
+
getDocumentState(instance, options),
|
|
203
|
+
// Pass options directly to getDocumentState for checking current value
|
|
204
|
+
shouldSuspend: (instance, {path: _path, ...options}: DocumentOptions<string | undefined>) =>
|
|
205
|
+
getDocumentState(instance, options).getCurrent() === undefined,
|
|
206
|
+
// Extract handle part for resolveDocument
|
|
207
|
+
suspender: (instance, options: DocumentOptions<string | undefined>) =>
|
|
208
|
+
resolveDocument(instance, options),
|
|
209
|
+
getConfig: identity as (
|
|
210
|
+
options: DocumentOptions<string | undefined>,
|
|
211
|
+
) => DocumentOptions<string | undefined>,
|
|
212
|
+
}) as UseDocument
|
|
@@ -33,11 +33,11 @@ describe('useDocumentEvent hook', () => {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
it('calls subscribeDocumentEvents with instance and a stable handler', () => {
|
|
36
|
-
const
|
|
36
|
+
const handleEvent = vi.fn()
|
|
37
37
|
const unsubscribe = vi.fn()
|
|
38
38
|
vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
|
|
39
39
|
|
|
40
|
-
renderHook(() => useDocumentEvent(
|
|
40
|
+
renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent}))
|
|
41
41
|
|
|
42
42
|
expect(vi.mocked(subscribeDocumentEvents)).toHaveBeenCalledTimes(1)
|
|
43
43
|
expect(vi.mocked(subscribeDocumentEvents).mock.calls[0][0]).toBe(instance)
|
|
@@ -47,15 +47,15 @@ describe('useDocumentEvent hook', () => {
|
|
|
47
47
|
|
|
48
48
|
const event = {type: 'edited', documentId: 'doc1', outgoing: {}} as DocumentEvent
|
|
49
49
|
stableHandler(event)
|
|
50
|
-
expect(
|
|
50
|
+
expect(handleEvent).toHaveBeenCalledWith(event)
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
it('calls the unsubscribe function on unmount', () => {
|
|
54
|
-
const
|
|
54
|
+
const handleEvent = vi.fn()
|
|
55
55
|
const unsubscribe = vi.fn()
|
|
56
56
|
vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
|
|
57
57
|
|
|
58
|
-
const {unmount} = renderHook(() => useDocumentEvent(
|
|
58
|
+
const {unmount} = renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent}))
|
|
59
59
|
unmount()
|
|
60
60
|
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
|
61
61
|
})
|
|
@@ -3,47 +3,91 @@ import {useCallback, useEffect, useInsertionEffect, useRef} from 'react'
|
|
|
3
3
|
|
|
4
4
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @beta
|
|
8
|
+
*/
|
|
9
|
+
export interface UseDocumentEventOptions<
|
|
10
|
+
TDataset extends string = string,
|
|
11
|
+
TProjectId extends string = string,
|
|
12
|
+
> extends DatasetHandle<TDataset, TProjectId> {
|
|
13
|
+
onEvent: (documentEvent: DocumentEvent) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
/**
|
|
7
17
|
*
|
|
8
18
|
* @beta
|
|
9
19
|
*
|
|
10
|
-
* Subscribes an event handler to events in your application's document store
|
|
11
|
-
* creation, deletion, and updates.
|
|
20
|
+
* Subscribes an event handler to events in your application's document store.
|
|
12
21
|
*
|
|
13
22
|
* @category Documents
|
|
14
|
-
* @param
|
|
15
|
-
* @
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* @param options - An object containing the event handler (`onEvent`) and optionally a `DatasetHandle` (projectId and dataset). If the handle is not provided, the nearest Sanity instance from context will be used.
|
|
24
|
+
* @example Creating a custom hook for document event toasts
|
|
25
|
+
* ```tsx
|
|
26
|
+
* import {createDatasetHandle, type DatasetHandle, type DocumentEvent, useDocumentEvent} from '@sanity/sdk-react'
|
|
27
|
+
* import {useToast} from './my-ui-library'
|
|
28
|
+
*
|
|
29
|
+
* // Define options for the custom hook, extending DatasetHandle
|
|
30
|
+
* interface DocumentToastsOptions extends DatasetHandle {
|
|
31
|
+
* // Could add more options, e.g., { includeEvents: DocumentEvent['type'][] }
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* // Define the custom hook
|
|
35
|
+
* function useDocumentToasts({...datasetHandle}: DocumentToastsOptions = {}) {
|
|
36
|
+
* const showToast = useToast() // Get the toast function
|
|
37
|
+
*
|
|
38
|
+
* // Define the event handler logic to show toasts on specific events
|
|
39
|
+
* const handleEvent = (event: DocumentEvent) => {
|
|
40
|
+
* if (event.type === 'published') {
|
|
41
|
+
* showToast(`Document ${event.documentId} published.`)
|
|
42
|
+
* } else if (event.type === 'unpublished') {
|
|
43
|
+
* showToast(`Document ${event.documentId} unpublished.`)
|
|
44
|
+
* } else if (event.type === 'deleted') {
|
|
45
|
+
* showToast(`Document ${event.documentId} deleted.`)
|
|
46
|
+
* } else {
|
|
47
|
+
* // Optionally log other events for debugging
|
|
48
|
+
* console.log('Document Event:', event.type, event.documentId)
|
|
49
|
+
* }
|
|
28
50
|
* }
|
|
29
|
-
*
|
|
51
|
+
*
|
|
52
|
+
* // Call the original hook, spreading the handle properties
|
|
53
|
+
* useDocumentEvent({
|
|
54
|
+
* ...datasetHandle, // Spread the dataset handle (projectId, dataset)
|
|
55
|
+
* onEvent: handleEvent,
|
|
56
|
+
* })
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* function MyComponentWithToasts() {
|
|
60
|
+
* // Use the custom hook, passing specific handle info
|
|
61
|
+
* const specificHandle = createDatasetHandle({ projectId: 'p1', dataset: 'ds1' })
|
|
62
|
+
* useDocumentToasts(specificHandle)
|
|
63
|
+
*
|
|
64
|
+
* // // Or use it relying on context for the handle
|
|
65
|
+
* // useDocumentToasts()
|
|
66
|
+
*
|
|
67
|
+
* return <div>...</div>
|
|
68
|
+
* }
|
|
30
69
|
* ```
|
|
31
70
|
*/
|
|
32
|
-
export function useDocumentEvent
|
|
33
|
-
|
|
34
|
-
|
|
71
|
+
export function useDocumentEvent<
|
|
72
|
+
TDataset extends string = string,
|
|
73
|
+
TProjectId extends string = string,
|
|
74
|
+
>(
|
|
75
|
+
// Single options object parameter
|
|
76
|
+
options: UseDocumentEventOptions<TDataset, TProjectId>,
|
|
35
77
|
): void {
|
|
36
|
-
|
|
78
|
+
// Destructure handler and datasetHandle from options
|
|
79
|
+
const {onEvent, ...datasetHandle} = options
|
|
80
|
+
const ref = useRef(onEvent)
|
|
37
81
|
|
|
38
82
|
useInsertionEffect(() => {
|
|
39
|
-
ref.current =
|
|
83
|
+
ref.current = onEvent
|
|
40
84
|
})
|
|
41
85
|
|
|
42
86
|
const stableHandler = useCallback((documentEvent: DocumentEvent) => {
|
|
43
87
|
return ref.current(documentEvent)
|
|
44
88
|
}, [])
|
|
45
89
|
|
|
46
|
-
const instance = useSanityInstance(
|
|
90
|
+
const instance = useSanityInstance(datasetHandle)
|
|
47
91
|
useEffect(() => {
|
|
48
92
|
return subscribeDocumentEvents(instance, stableHandler)
|
|
49
93
|
}, [instance, stableHandler])
|
|
@@ -11,23 +11,41 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
11
11
|
* Check if the current user has the specified permissions for the given document actions.
|
|
12
12
|
*
|
|
13
13
|
* @category Permissions
|
|
14
|
-
* @param actionOrActions - One
|
|
14
|
+
* @param actionOrActions - One or more document action functions (e.g., `publishDocument(handle)`).
|
|
15
15
|
* @returns An object that specifies whether the action is allowed; if the action is not allowed, an explanatory message and list of reasons is also provided.
|
|
16
16
|
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* When passing multiple actions, all actions must belong to the same project and dataset.
|
|
19
|
+
* Note, however, that you can check permissions on multiple documents from the same project and dataset (as in the second example below).
|
|
20
|
+
*
|
|
17
21
|
* @example Checking for permission to publish a document
|
|
18
|
-
* ```
|
|
19
|
-
* import {
|
|
20
|
-
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* import {
|
|
24
|
+
* useDocumentPermissions,
|
|
25
|
+
* useApplyDocumentActions,
|
|
26
|
+
* publishDocument,
|
|
27
|
+
* createDocumentHandle,
|
|
28
|
+
* type DocumentHandle
|
|
29
|
+
* } from '@sanity/sdk-react'
|
|
30
|
+
*
|
|
31
|
+
* // Define props using the DocumentHandle type
|
|
32
|
+
* interface PublishButtonProps {
|
|
33
|
+
* doc: DocumentHandle
|
|
34
|
+
* }
|
|
21
35
|
*
|
|
22
|
-
*
|
|
23
|
-
* const
|
|
24
|
-
*
|
|
36
|
+
* function PublishButton({doc}: PublishButtonProps) {
|
|
37
|
+
* const publishAction = publishDocument(doc)
|
|
38
|
+
*
|
|
39
|
+
* // Pass the same action call to check permissions
|
|
40
|
+
* const publishPermissions = useDocumentPermissions(publishAction)
|
|
41
|
+
* const apply = useApplyDocumentActions()
|
|
25
42
|
*
|
|
26
43
|
* return (
|
|
27
44
|
* <>
|
|
28
45
|
* <button
|
|
29
46
|
* disabled={!publishPermissions.allowed}
|
|
30
|
-
*
|
|
47
|
+
* // Pass the same action call to apply the action
|
|
48
|
+
* onClick={() => apply(publishAction)}
|
|
31
49
|
* popoverTarget={`${publishPermissions.allowed ? undefined : 'publishButtonPopover'}`}
|
|
32
50
|
* >
|
|
33
51
|
* Publish
|
|
@@ -40,6 +58,27 @@ import {useSanityInstance} from '../context/useSanityInstance'
|
|
|
40
58
|
* </>
|
|
41
59
|
* )
|
|
42
60
|
* }
|
|
61
|
+
*
|
|
62
|
+
* // Usage:
|
|
63
|
+
* // const doc = createDocumentHandle({ documentId: 'doc1', documentType: 'myType' })
|
|
64
|
+
* // <PublishButton doc={doc} />
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example Checking for permissions to edit multiple documents
|
|
68
|
+
* ```tsx
|
|
69
|
+
* import {
|
|
70
|
+
* useDocumentPermissions,
|
|
71
|
+
* editDocument,
|
|
72
|
+
* type DocumentHandle
|
|
73
|
+
* } from '@sanity/sdk-react'
|
|
74
|
+
*
|
|
75
|
+
* export default function canEditMultiple(docHandles: DocumentHandle[]) {
|
|
76
|
+
* // Create an array containing an editDocument action for each of the document handles
|
|
77
|
+
* const editActions = docHandles.map(doc => editDocument(doc))
|
|
78
|
+
*
|
|
79
|
+
* // Return the result of checking for edit permissions on all of the document handles
|
|
80
|
+
* return useDocumentPermissions(editActions)
|
|
81
|
+
* }
|
|
43
82
|
* ```
|
|
44
83
|
*/
|
|
45
84
|
export function useDocumentPermissions(
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import {getDocumentSyncStatus} from '@sanity/sdk'
|
|
2
|
-
import {identity} from 'rxjs'
|
|
3
2
|
import {describe, it} from 'vitest'
|
|
4
3
|
|
|
5
4
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
const mockHook = vi.fn()
|
|
7
|
+
vi.mock('../helpers/createStateSourceHook', () => ({createStateSourceHook: vi.fn(() => mockHook)}))
|
|
8
8
|
vi.mock('@sanity/sdk', () => ({getDocumentSyncStatus: vi.fn()}))
|
|
9
9
|
|
|
10
10
|
describe('useDocumentSyncStatus', () => {
|
|
11
|
-
it('calls `createStateSourceHook` with `
|
|
11
|
+
it('calls `createStateSourceHook` with `getDocumentSyncStatus`', async () => {
|
|
12
12
|
const {useDocumentSyncStatus} = await import('./useDocumentSyncStatus')
|
|
13
|
-
expect(createStateSourceHook).toHaveBeenCalledWith(
|
|
14
|
-
|
|
13
|
+
expect(createStateSourceHook).toHaveBeenCalledWith(
|
|
14
|
+
expect.objectContaining({
|
|
15
|
+
getState: getDocumentSyncStatus,
|
|
16
|
+
shouldSuspend: expect.any(Function),
|
|
17
|
+
suspender: expect.any(Function),
|
|
18
|
+
getConfig: expect.any(Function),
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
expect(useDocumentSyncStatus).toBe(mockHook)
|
|
15
22
|
})
|
|
16
23
|
})
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type DocumentHandle,
|
|
3
|
+
getDocumentSyncStatus,
|
|
4
|
+
resolveDocument,
|
|
5
|
+
type SanityInstance,
|
|
6
|
+
type StateSource,
|
|
7
|
+
} from '@sanity/sdk'
|
|
8
|
+
import {identity} from 'rxjs'
|
|
2
9
|
|
|
3
10
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
4
11
|
|
|
@@ -10,25 +17,45 @@ type UseDocumentSyncStatus = {
|
|
|
10
17
|
* @param doc - The document handle to get sync status for. If you pass a `DocumentHandle` with specified `projectId` and `dataset`,
|
|
11
18
|
* the document will be read from the specified Sanity project and dataset that is included in the handle. If no `projectId` or `dataset` is provided,
|
|
12
19
|
* the document will use the nearest instance from context.
|
|
13
|
-
* @returns `true` if local changes are synced with remote, `false` if
|
|
14
|
-
* @example
|
|
15
|
-
* ```
|
|
16
|
-
*
|
|
17
|
-
*
|
|
20
|
+
* @returns `true` if local changes are synced with remote, `false` if changes are pending. Note: Suspense handles loading states.
|
|
21
|
+
* @example Show sync status indicator
|
|
22
|
+
* ```tsx
|
|
23
|
+
* import {useDocumentSyncStatus, createDocumentHandle, type DocumentHandle} from '@sanity/sdk-react'
|
|
24
|
+
*
|
|
25
|
+
* // Define props including the DocumentHandle type
|
|
26
|
+
* interface SyncIndicatorProps {
|
|
27
|
+
* doc: DocumentHandle
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* function SyncIndicator({doc}: SyncIndicatorProps) {
|
|
31
|
+
* const isSynced = useDocumentSyncStatus(doc)
|
|
32
|
+
*
|
|
33
|
+
* return (
|
|
34
|
+
* <div className={`sync-status ${isSynced ? 'synced' : 'pending'}`}>
|
|
35
|
+
* {isSynced ? '✓ Synced' : 'Saving changes...'}
|
|
36
|
+
* </div>
|
|
37
|
+
* )
|
|
38
|
+
* }
|
|
18
39
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* </button>
|
|
23
|
-
* )
|
|
40
|
+
* // Usage:
|
|
41
|
+
* // const doc = createDocumentHandle({ documentId: 'doc1', documentType: 'myType' })
|
|
42
|
+
* // <SyncIndicator doc={doc} />
|
|
24
43
|
* ```
|
|
25
44
|
*/
|
|
26
|
-
(doc: DocumentHandle): boolean
|
|
45
|
+
(doc: DocumentHandle): boolean
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
/**
|
|
30
49
|
* @beta
|
|
31
50
|
* @function
|
|
32
51
|
*/
|
|
33
|
-
export const useDocumentSyncStatus: UseDocumentSyncStatus =
|
|
34
|
-
|
|
52
|
+
export const useDocumentSyncStatus: UseDocumentSyncStatus = createStateSourceHook({
|
|
53
|
+
getState: getDocumentSyncStatus as (
|
|
54
|
+
instance: SanityInstance,
|
|
55
|
+
doc: DocumentHandle,
|
|
56
|
+
) => StateSource<boolean>,
|
|
57
|
+
shouldSuspend: (instance, doc: DocumentHandle) =>
|
|
58
|
+
getDocumentSyncStatus(instance, doc).getCurrent() === undefined,
|
|
59
|
+
suspender: (instance, doc: DocumentHandle) => resolveDocument(instance, doc),
|
|
60
|
+
getConfig: identity,
|
|
61
|
+
})
|