@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.
Files changed (40) hide show
  1. package/README.md +5 -57
  2. package/dist/index.d.ts +1000 -438
  3. package/dist/index.js +324 -258
  4. package/dist/index.js.map +1 -1
  5. package/package.json +17 -16
  6. package/src/_exports/sdk-react.ts +4 -1
  7. package/src/components/SDKProvider.tsx +6 -1
  8. package/src/components/SanityApp.test.tsx +29 -47
  9. package/src/components/SanityApp.tsx +12 -11
  10. package/src/components/auth/AuthBoundary.test.tsx +177 -7
  11. package/src/components/auth/AuthBoundary.tsx +32 -2
  12. package/src/components/auth/ConfigurationError.ts +22 -0
  13. package/src/components/auth/LoginError.tsx +9 -3
  14. package/src/hooks/auth/useVerifyOrgProjects.test.tsx +136 -0
  15. package/src/hooks/auth/useVerifyOrgProjects.tsx +48 -0
  16. package/src/hooks/client/useClient.ts +3 -3
  17. package/src/hooks/comlink/useManageFavorite.test.ts +276 -27
  18. package/src/hooks/comlink/useManageFavorite.ts +102 -51
  19. package/src/hooks/comlink/useWindowConnection.ts +3 -2
  20. package/src/hooks/document/useApplyDocumentActions.ts +105 -31
  21. package/src/hooks/document/useDocument.test.ts +41 -4
  22. package/src/hooks/document/useDocument.ts +198 -114
  23. package/src/hooks/document/useDocumentEvent.test.ts +5 -5
  24. package/src/hooks/document/useDocumentEvent.ts +67 -23
  25. package/src/hooks/document/useDocumentPermissions.ts +47 -8
  26. package/src/hooks/document/useDocumentSyncStatus.test.ts +12 -5
  27. package/src/hooks/document/useDocumentSyncStatus.ts +41 -14
  28. package/src/hooks/document/useEditDocument.test.ts +24 -6
  29. package/src/hooks/document/useEditDocument.ts +238 -133
  30. package/src/hooks/documents/useDocuments.test.tsx +1 -1
  31. package/src/hooks/documents/useDocuments.ts +153 -44
  32. package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +1 -1
  33. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +120 -47
  34. package/src/hooks/projection/useProjection.ts +134 -46
  35. package/src/hooks/query/useQuery.test.tsx +4 -4
  36. package/src/hooks/query/useQuery.ts +115 -43
  37. package/src/hooks/releases/useActiveReleases.test.tsx +84 -0
  38. package/src/hooks/releases/useActiveReleases.ts +39 -0
  39. package/src/hooks/releases/usePerspective.test.tsx +120 -0
  40. package/src/hooks/releases/usePerspective.ts +50 -0
@@ -1,128 +1,212 @@
1
- import {
2
- type DocumentHandle,
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
- * @beta
15
- *
16
- * ## useDocument(doc, path)
17
- * Read and subscribe to nested values in a document
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
- * @beta
51
- * ## useDocument(doc)
52
- * Read and subscribe to an entire document
53
- * @param doc - The document to read state from, specified as a DocumentHandle
54
- * @returns The document state as an object
55
- * @example
56
- * ```tsx
57
- * import {type SanityDocument, useDocument} from '@sanity/sdk-react'
58
- *
59
- * interface Book extends SanityDocument {
60
- * title: string
61
- * author: string
62
- * summary: string
63
- * }
64
- *
65
- * const documentHandle = {
66
- * documentId: 'book-123',
67
- * documentType: 'book',
68
- * projectId: 'abc123',
69
- * dataset: 'production'
70
- * }
71
- *
72
- * function DocumentView() {
73
- * const book = useDocument<Book>(documentHandle)
74
- *
75
- * if (!book) {
76
- * return <div>Loading...</div>
77
- * }
78
- *
79
- * return (
80
- * <article>
81
- * <h1>{book.title}</h1>
82
- * <address>By {book.author}</address>
83
- *
84
- * <h2>Summary</h2>
85
- * {book.summary}
86
- *
87
- * <h2>Order</h2>
88
- * <a href={`/order/${book._id}`}>Order {book.title} today!</a>
89
- * </article>
90
- * )
91
- * }
92
- * ```
93
- *
94
- */
95
- export function useDocument<TDocument extends SanityDocument>(
96
- doc: DocumentHandle<TDocument>,
97
- ): TDocument | null
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
- * When called with a `path` argument, the hook will return the nested value's state.
103
- * When called without a `path` argument, the entire document's state will be returned.
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 designed to be used within a realtime context in which local updates to documents
107
- * need to be displayed before they are persisted to the remote copy. This can be useful within a collaborative
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
- * The hook automatically uses the correct Sanity instance based on the project and dataset
111
- * specified in the DocumentHandle. This makes it easy to work with documents from different
112
- * projects or datasets in the same component.
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
- * However, this hook can be too resource intensive for applications where static document values simply
115
- * need to be displayed (or when changes to documents don't need to be reflected immediately);
116
- * consider using `usePreview` or `useQuery` for these use cases instead. These hooks leverage the Sanity
117
- * Live Content API to provide a more efficient way to read and subscribe to document state.
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 function useDocument(doc: DocumentHandle, path?: string): unknown {
120
- return _useDocument(doc, path)
121
- }
122
-
123
- const _useDocument = createStateSourceHook<[doc: DocumentHandle, path?: string], unknown>({
124
- getState: getDocumentState,
125
- shouldSuspend: (instance, doc) => getDocumentState(instance, doc).getCurrent() === undefined,
126
- suspender: resolveDocument,
127
- getConfig: identity,
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 handler = vi.fn()
36
+ const handleEvent = vi.fn()
37
37
  const unsubscribe = vi.fn()
38
38
  vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
39
39
 
40
- renderHook(() => useDocumentEvent(handler, docHandle))
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(handler).toHaveBeenCalledWith(event)
50
+ expect(handleEvent).toHaveBeenCalledWith(event)
51
51
  })
52
52
 
53
53
  it('calls the unsubscribe function on unmount', () => {
54
- const handler = vi.fn()
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(handler, docHandle))
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, such as document
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 handler - The event handler to register.
15
- * @param doc - The document to subscribe to events for. If you pass a `DocumentHandle` with specified `projectId` and `dataset`,
16
- * 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,
17
- * the document will use the nearest instance from context.
18
- * @example
19
- * ```
20
- * import {useDocumentEvent} from '@sanity/sdk-react'
21
- * import {type DocumentEvent} from '@sanity/sdk'
22
- *
23
- * useDocumentEvent((event) => {
24
- * if (event.type === DocumentEvent.DocumentDeletedEvent) {
25
- * alert(`Document with ID ${event.documentId} deleted!`)
26
- * } else {
27
- * console.log(event)
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
- handler: (documentEvent: DocumentEvent) => void,
34
- dataset: DatasetHandle,
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
- const ref = useRef(handler)
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 = handler
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(dataset)
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 more more calls to a particular document action function for a given document
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
- * ```ts
19
- * import {useDocumentPermissions, useApplyDocumentActions} from '@sanity/sdk-react'
20
- * import {publishDocument} from '@sanity/sdk'
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
- * export function PublishButton({doc}: {doc: DocumentHandle}) {
23
- * const publishPermissions = useDocumentPermissions(publishDocument(doc))
24
- * const applyAction = useApplyDocumentActions()
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
- * onClick={() => applyAction(publishDocument(doc))}
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
- vi.mock('../helpers/createStateSourceHook', () => ({createStateSourceHook: vi.fn(identity)}))
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 `getTokenState`', async () => {
11
+ it('calls `createStateSourceHook` with `getDocumentSyncStatus`', async () => {
12
12
  const {useDocumentSyncStatus} = await import('./useDocumentSyncStatus')
13
- expect(createStateSourceHook).toHaveBeenCalledWith(getDocumentSyncStatus)
14
- expect(useDocumentSyncStatus).toBe(getDocumentSyncStatus)
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 {type DocumentHandle, getDocumentSyncStatus} from '@sanity/sdk'
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 the changes are not synced, and `undefined` if the document is not found
14
- * @example Disable a Save button when there are no changes to sync
15
- * ```
16
- * const myDocumentHandle = { documentId: 'documentId', documentType: 'documentType', projectId: 'projectId', dataset: 'dataset' }
17
- * const documentSynced = useDocumentSyncStatus(myDocumentHandle)
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
- * return (
20
- * <button disabled={documentSynced}>
21
- * Save Changes
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 | undefined
45
+ (doc: DocumentHandle): boolean
27
46
  }
28
47
 
29
48
  /**
30
49
  * @beta
31
50
  * @function
32
51
  */
33
- export const useDocumentSyncStatus: UseDocumentSyncStatus =
34
- createStateSourceHook(getDocumentSyncStatus)
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
+ })