@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.2
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 +103 -155
- package/dist/index.d.ts +2488 -2019
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4435 -1529
- package/dist/index.js.map +1 -1
- package/package.json +55 -59
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +30 -18
- package/src/components/SDKProvider.test.tsx +173 -32
- package/src/components/SDKProvider.tsx +87 -28
- package/src/components/SanityApp.test.tsx +68 -12
- package/src/components/SanityApp.tsx +93 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +7 -7
- package/src/components/auth/AuthBoundary.tsx +29 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +104 -61
- package/src/components/errors/ChunkLoadError.test.tsx +59 -0
- package/src/components/errors/ChunkLoadError.tsx +56 -0
- package/src/components/errors/CorsErrorComponent.tsx +2 -2
- package/src/components/errors/chunkReloadStorage.ts +57 -0
- package/src/config/handles.ts +29 -27
- package/src/constants.ts +5 -0
- package/src/context/ComlinkTokenRefresh.test.tsx +10 -27
- package/src/context/ComlinkTokenRefresh.tsx +3 -5
- package/src/context/DefaultResourceContext.ts +3 -3
- package/src/context/OrganizationResourcesProvider.test.tsx +189 -0
- package/src/context/OrganizationResourcesProvider.tsx +111 -0
- package/src/context/PerspectiveContext.ts +3 -3
- package/src/context/ProjectContext.ts +15 -0
- package/src/context/ResourceProvider.test.tsx +93 -90
- package/src/context/ResourceProvider.tsx +62 -93
- package/src/context/SDKStudioContext.test.tsx +28 -33
- package/src/context/SanityInstanceProvider.test.tsx +100 -0
- package/src/context/SanityInstanceProvider.tsx +71 -0
- package/src/context/WorkbenchTokenRefresh.test.tsx +106 -0
- package/src/context/WorkbenchTokenRefresh.tsx +61 -0
- package/src/context/renderSanityApp.test.tsx +151 -49
- package/src/context/renderSanityApp.tsx +12 -8
- package/src/context/workbenchToken.ts +63 -0
- package/src/hooks/access/useCheckPermissions.test-d.ts +15 -0
- package/src/hooks/access/useCheckPermissions.test.tsx +53 -0
- package/src/hooks/access/useCheckPermissions.ts +24 -0
- package/src/hooks/agent/agentActions.test.tsx +2 -2
- package/src/hooks/agent/agentActions.ts +52 -72
- package/src/hooks/agent/useAgentResourceContext.ts +1 -1
- package/src/hooks/applications/useApplication.test-d.ts +30 -0
- package/src/hooks/applications/useApplication.ts +22 -0
- package/src/hooks/applications/useApplications.test-d.ts +31 -0
- package/src/hooks/applications/useApplications.ts +25 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/auth/useDashboardOrganizationId.test.tsx +2 -8
- package/src/hooks/auth/useVerifyOrgProjects.test.tsx +8 -32
- package/src/hooks/auth/useVerifyOrgProjects.tsx +13 -6
- package/src/hooks/client/useClient.test.tsx +1 -4
- package/src/hooks/client/useClient.ts +1 -0
- package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
- package/src/hooks/comlink/useFrameConnection.ts +1 -1
- package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
- package/src/hooks/comlink/useWindowConnection.ts +2 -3
- package/src/hooks/comments/useCommentActions.test.tsx +150 -0
- package/src/hooks/comments/useCommentActions.ts +109 -0
- package/src/hooks/comments/useCommentList.ts +79 -0
- package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
- package/src/hooks/comments/useCommentThreads.ts +73 -0
- package/src/hooks/comments/useComments.test.tsx +242 -0
- package/src/hooks/comments/useComments.ts +59 -0
- package/src/hooks/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +18 -1
- package/src/hooks/context/useSanityInstance.ts +6 -6
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +11 -17
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +5 -7
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -6
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +0 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -2
- package/src/hooks/dashboard/useWindowTitle.test.ts +244 -0
- package/src/hooks/dashboard/useWindowTitle.ts +114 -0
- package/src/hooks/datasets/useDatasets.test.tsx +116 -0
- package/src/hooks/datasets/useDatasets.ts +33 -13
- package/src/hooks/document/useApplyDocumentActions.test.tsx +48 -0
- package/src/hooks/document/useApplyDocumentActions.ts +66 -48
- package/src/hooks/document/useCreateDocument.test.tsx +83 -0
- package/src/hooks/document/useCreateDocument.ts +117 -0
- package/src/hooks/document/useDocument.test.tsx +37 -8
- package/src/hooks/document/useDocument.ts +131 -66
- package/src/hooks/document/useDocumentEvent.test.tsx +16 -4
- package/src/hooks/document/useDocumentEvent.ts +16 -10
- package/src/hooks/document/useDocumentPermissions.test.tsx +8 -7
- package/src/hooks/document/useDocumentPermissions.ts +71 -39
- package/src/hooks/document/useDocumentSyncStatus.test.ts +1 -0
- package/src/hooks/document/useDocumentSyncStatus.ts +13 -9
- package/src/hooks/document/useEditDocument.test.tsx +21 -14
- package/src/hooks/document/useEditDocument.ts +163 -17
- package/src/hooks/documents/useDocuments.test.tsx +64 -44
- package/src/hooks/documents/useDocuments.ts +31 -20
- package/src/hooks/helpers/createCallbackHook.test.tsx +13 -19
- package/src/hooks/helpers/createFetcherHook.test.tsx +180 -0
- package/src/hooks/helpers/createFetcherHook.ts +69 -0
- package/src/hooks/helpers/createMutationHook.test.tsx +125 -0
- package/src/hooks/helpers/createMutationHook.tsx +93 -0
- package/src/hooks/helpers/createStateSourceHook.test.tsx +10 -10
- package/src/hooks/helpers/createStateSourceHook.tsx +2 -1
- package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx} +92 -53
- package/src/hooks/helpers/useApplyActions.ts +68 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +226 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +79 -48
- package/src/hooks/helpers/useResolvedProjectId.test.tsx +59 -0
- package/src/hooks/helpers/useResolvedProjectId.ts +35 -0
- package/src/hooks/helpers/useTrackHookUsage.ts +37 -0
- package/src/hooks/installations/useInstallation.test-d.ts +19 -0
- package/src/hooks/installations/useInstallation.ts +22 -0
- package/src/hooks/installations/useInstallations.test-d.ts +26 -0
- package/src/hooks/installations/useInstallations.ts +25 -0
- package/src/hooks/organizations/useOrganization.test-d.ts +60 -0
- package/src/hooks/organizations/useOrganization.test.ts +63 -0
- package/src/hooks/organizations/useOrganization.ts +34 -0
- package/src/hooks/organizations/useOrganizations.test-d.ts +68 -0
- package/src/hooks/organizations/useOrganizations.test.ts +63 -0
- package/src/hooks/organizations/useOrganizations.ts +39 -0
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.test.tsx +34 -27
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +35 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +22 -20
- package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
- package/src/hooks/presence/usePresenceForDocument.ts +104 -0
- package/src/hooks/presence/useReportPresence.test.tsx +202 -0
- package/src/hooks/presence/useReportPresence.ts +170 -0
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +12 -14
- package/src/hooks/projection/useDocumentProjection.test.tsx +78 -9
- package/src/hooks/projection/useDocumentProjection.ts +81 -2
- package/src/hooks/projects/useProject.test-d.ts +56 -0
- package/src/hooks/projects/useProject.test.tsx +120 -0
- package/src/hooks/projects/useProject.ts +42 -43
- package/src/hooks/projects/useProjects.test-d.ts +60 -0
- package/src/hooks/projects/useProjects.test.ts +45 -98
- package/src/hooks/projects/useProjects.ts +17 -32
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +94 -17
- package/src/hooks/releases/useActiveReleases.test.tsx +13 -7
- package/src/hooks/releases/useActiveReleases.ts +28 -24
- package/src/hooks/releases/useAllReleases.test.tsx +93 -0
- package/src/hooks/releases/useAllReleases.ts +62 -0
- package/src/hooks/releases/useApplyReleaseActions.test.tsx +66 -0
- package/src/hooks/releases/useApplyReleaseActions.ts +82 -0
- package/src/hooks/releases/usePerspective.test.tsx +18 -10
- package/src/hooks/releases/usePerspective.ts +13 -17
- package/src/hooks/users/useUser.test.tsx +3 -9
- package/src/hooks/users/useUser.ts +4 -8
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +38 -5
- package/src/utils/resolveOrgResources.test.ts +111 -0
- package/src/utils/resolveOrgResources.ts +69 -0
- package/src/hooks/context/useDefaultResource.test.tsx +0 -25
- package/src/hooks/context/useDefaultResource.ts +0 -30
- package/src/hooks/dashboard/useDispatchIntent.test.ts +0 -254
- package/src/hooks/dashboard/useDispatchIntent.ts +0 -157
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.test.ts +0 -107
- package/src/hooks/dashboard/utils/useResourceIdFromDocumentHandle.ts +0 -39
- package/src/hooks/datasets/useDatasets.test.ts +0 -66
- package/src/hooks/helpers/useNormalizedResourceOptions.test.ts +0 -65
- package/src/hooks/projects/useProject.test.ts +0 -79
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {type DocumentOptions, getDocumentState, resolveDocument} from '@sanity/sdk'
|
|
2
|
-
import {type SanityDocument} from '
|
|
1
|
+
import {type DocumentOptions, getDocumentState, type JsonMatch, resolveDocument} from '@sanity/sdk'
|
|
2
|
+
import {type SanityDocument} from 'groq'
|
|
3
|
+
import {identity} from 'rxjs'
|
|
3
4
|
|
|
4
5
|
import {type DocumentHandle} from '../../config/handles'
|
|
5
6
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
6
7
|
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
8
|
+
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
7
9
|
// used in an `{@link useDocumentProjection}` and `{@link useQuery}`
|
|
8
|
-
// eslint-disable-next-line import/consistent-type-specifier-style
|
|
10
|
+
// eslint-disable-next-line import-x/consistent-type-specifier-style
|
|
9
11
|
import type {useDocumentProjection} from '../projection/useDocumentProjection'
|
|
10
|
-
// eslint-disable-next-line import/consistent-type-specifier-style
|
|
12
|
+
// eslint-disable-next-line import-x/consistent-type-specifier-style
|
|
11
13
|
import type {useQuery} from '../query/useQuery'
|
|
12
14
|
|
|
13
15
|
const useDocumentValue = createStateSourceHook({
|
|
@@ -20,6 +22,9 @@ const useDocumentValue = createStateSourceHook({
|
|
|
20
22
|
// Extract handle part for resolveDocument
|
|
21
23
|
suspender: (instance, options: DocumentOptions<string | undefined>) =>
|
|
22
24
|
resolveDocument(instance, options),
|
|
25
|
+
getConfig: identity as (
|
|
26
|
+
options: DocumentOptions<string | undefined>,
|
|
27
|
+
) => DocumentOptions<string | undefined>,
|
|
23
28
|
})
|
|
24
29
|
|
|
25
30
|
const wrapHookWithData = <TParams extends unknown[], TReturn>(
|
|
@@ -39,108 +44,168 @@ type UseDocumentOptions<
|
|
|
39
44
|
> = DocumentHandle<TDocumentType, TDataset, TProjectId> & {path?: TPath}
|
|
40
45
|
|
|
41
46
|
interface UseDocument {
|
|
47
|
+
/** @internal */
|
|
48
|
+
<TDocumentType extends string, TDataset extends string, TProjectId extends string = string>(
|
|
49
|
+
options: UseDocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
|
|
50
|
+
): {data: SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null}
|
|
51
|
+
|
|
52
|
+
/** @internal */
|
|
53
|
+
<
|
|
54
|
+
TPath extends string,
|
|
55
|
+
TDocumentType extends string,
|
|
56
|
+
TDataset extends string = string,
|
|
57
|
+
TProjectId extends string = string,
|
|
58
|
+
>(
|
|
59
|
+
options: UseDocumentOptions<TPath, TDocumentType>,
|
|
60
|
+
): {
|
|
61
|
+
data: JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath> | undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @internal */
|
|
65
|
+
<TData>(options: DocumentOptions<undefined>): {data: TData | null}
|
|
66
|
+
/** @internal */
|
|
67
|
+
<TData>(options: DocumentOptions<string>): {data: TData | undefined}
|
|
68
|
+
|
|
42
69
|
/**
|
|
70
|
+
* ## useDocument via Type Inference (Recommended)
|
|
71
|
+
*
|
|
43
72
|
* @public
|
|
44
73
|
*
|
|
45
|
-
*
|
|
74
|
+
* The preferred way to use this hook when working with Sanity Typegen.
|
|
46
75
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
76
|
+
* Features:
|
|
77
|
+
* - Automatically infers document types from your schema
|
|
78
|
+
* - Provides type-safe access to documents and nested fields
|
|
79
|
+
* - Supports project/dataset-specific type inference
|
|
80
|
+
* - Works seamlessly with Typegen-generated types
|
|
49
81
|
*
|
|
50
|
-
* This hook will suspend while the document data is being fetched.
|
|
82
|
+
* This hook will suspend while the document data is being fetched and loaded.
|
|
83
|
+
*
|
|
84
|
+
* When fetching a full document:
|
|
85
|
+
* - Returns the complete document object if it exists
|
|
86
|
+
* - Returns `null` if the document doesn't exist
|
|
51
87
|
*
|
|
52
|
-
*
|
|
88
|
+
* When fetching with a path:
|
|
89
|
+
* - Returns the value at the specified path if both the document and path exist
|
|
90
|
+
* - Returns `undefined` if either the document doesn't exist or the path doesn't exist in the document
|
|
91
|
+
*
|
|
92
|
+
* @category Documents
|
|
53
93
|
* @param options - Configuration including `documentId`, `documentType`, and optionally:
|
|
94
|
+
* - `path`: To select a nested value (returns typed value at path)
|
|
54
95
|
* - `projectId`/`dataset`: For multi-project/dataset setups
|
|
55
|
-
* @returns The document state
|
|
96
|
+
* @returns The document state (or nested value if path provided).
|
|
56
97
|
*
|
|
57
|
-
* @example Basic document fetch
|
|
98
|
+
* @example Basic document fetch
|
|
58
99
|
* ```tsx
|
|
59
|
-
* import {useDocument, type DocumentHandle
|
|
60
|
-
*
|
|
61
|
-
* interface Book extends SanityDocument {
|
|
62
|
-
* _type: 'book'
|
|
63
|
-
* title: string
|
|
64
|
-
* author: string
|
|
65
|
-
* }
|
|
100
|
+
* import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
|
|
66
101
|
*
|
|
67
|
-
* interface
|
|
68
|
-
* doc: DocumentHandle
|
|
102
|
+
* interface ProductViewProps {
|
|
103
|
+
* doc: DocumentHandle<'product'> // Typegen infers product type
|
|
69
104
|
* }
|
|
70
105
|
*
|
|
71
|
-
* function
|
|
72
|
-
* const {data:
|
|
73
|
-
* return <h1>{
|
|
106
|
+
* function ProductView({doc}: ProductViewProps) {
|
|
107
|
+
* const {data: product} = useDocument({...doc}) // Fully typed product
|
|
108
|
+
* return <h1>{product.title ?? 'Untitled'}</h1>
|
|
74
109
|
* }
|
|
75
110
|
* ```
|
|
76
111
|
*
|
|
77
|
-
* @
|
|
78
|
-
*/
|
|
79
|
-
<TData>(options: UseDocumentOptions<undefined>): {data: TData | null}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* @public
|
|
83
|
-
*
|
|
84
|
-
* ## useDocument with Explicit Types (with path)
|
|
85
|
-
*
|
|
86
|
-
* Provide an explicit type parameter `TData` to type the returned field data
|
|
87
|
-
* when using a `path` to select a nested value.
|
|
88
|
-
*
|
|
89
|
-
* @typeParam TData - The explicit type for the field
|
|
90
|
-
* @param options - Configuration including `documentId`, `documentType`, `path`, and optionally:
|
|
91
|
-
* - `projectId`/`dataset`: For multi-project/dataset setups
|
|
92
|
-
* @returns The field value at the specified path
|
|
93
|
-
*
|
|
94
|
-
* @example Fetching a specific field with explicit type
|
|
112
|
+
* @example Fetching a specific field
|
|
95
113
|
* ```tsx
|
|
96
114
|
* import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
|
|
97
115
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
116
|
+
* interface ProductTitleProps {
|
|
117
|
+
* doc: DocumentHandle<'product'>
|
|
118
|
+
* }
|
|
119
|
+
*
|
|
120
|
+
* function ProductTitle({doc}: ProductTitleProps) {
|
|
121
|
+
* const {data: title} = useDocument({
|
|
122
|
+
* ...doc,
|
|
123
|
+
* path: 'title' // Returns just the title field
|
|
124
|
+
* })
|
|
100
125
|
* return <h1>{title ?? 'Untitled'}</h1>
|
|
101
126
|
* }
|
|
102
127
|
* ```
|
|
103
128
|
*
|
|
104
129
|
* @inlineType DocumentOptions
|
|
105
130
|
*/
|
|
106
|
-
<
|
|
131
|
+
<
|
|
132
|
+
TPath extends string | undefined = undefined,
|
|
133
|
+
TDocumentType extends string = string,
|
|
134
|
+
TDataset extends string = string,
|
|
135
|
+
TProjectId extends string = string,
|
|
136
|
+
>(
|
|
137
|
+
options: UseDocumentOptions<TPath, TDocumentType>,
|
|
138
|
+
): TPath extends string
|
|
139
|
+
? {
|
|
140
|
+
data:
|
|
141
|
+
| JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath>
|
|
142
|
+
| undefined
|
|
143
|
+
}
|
|
144
|
+
: {data: SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`> | null}
|
|
107
145
|
|
|
108
146
|
/**
|
|
109
147
|
* @public
|
|
110
148
|
*
|
|
111
|
-
* ## useDocument
|
|
149
|
+
* ## useDocument via Explicit Types
|
|
112
150
|
*
|
|
113
|
-
*
|
|
151
|
+
* Use this version when:
|
|
152
|
+
* - You're not using Sanity Typegen
|
|
153
|
+
* - You need to manually specify document types
|
|
154
|
+
* - You're working with dynamic document types
|
|
114
155
|
*
|
|
115
|
-
*
|
|
156
|
+
* Key differences from Typegen version:
|
|
157
|
+
* - Requires manual type specification via `TData`
|
|
158
|
+
* - Returns `TData | null` for full documents
|
|
159
|
+
* - Returns `TData | undefined` for nested values
|
|
116
160
|
*
|
|
117
|
-
*
|
|
118
|
-
* - Returns the complete document object if it exists
|
|
119
|
-
* - Returns `null` if the document doesn't exist
|
|
120
|
-
*
|
|
121
|
-
* When fetching with a path:
|
|
122
|
-
* - Returns the value at the specified path if both the document and path exist
|
|
123
|
-
* - Returns `undefined` if either the document doesn't exist or the path doesn't exist in the document
|
|
161
|
+
* This hook will suspend while the document data is being fetched.
|
|
124
162
|
*
|
|
125
|
-
* @
|
|
126
|
-
* @
|
|
163
|
+
* @typeParam TData - The explicit type for the document or field
|
|
164
|
+
* @typeParam TPath - Optional path to a nested value
|
|
165
|
+
* @param options - Configuration including `documentId` and optionally:
|
|
127
166
|
* - `path`: To select a nested value
|
|
128
167
|
* - `projectId`/`dataset`: For multi-project/dataset setups
|
|
129
|
-
* @returns The document state (or nested value if path provided)
|
|
168
|
+
* @returns The document state (or nested value if path provided)
|
|
130
169
|
*
|
|
131
|
-
* @example Basic document fetch
|
|
170
|
+
* @example Basic document fetch with explicit type
|
|
171
|
+
* ```tsx
|
|
172
|
+
* import {useDocument, type DocumentHandle, type SanityDocument} from '@sanity/sdk-react'
|
|
173
|
+
*
|
|
174
|
+
* interface Book extends SanityDocument {
|
|
175
|
+
* _type: 'book'
|
|
176
|
+
* title: string
|
|
177
|
+
* author: string
|
|
178
|
+
* }
|
|
179
|
+
*
|
|
180
|
+
* interface BookViewProps {
|
|
181
|
+
* doc: DocumentHandle
|
|
182
|
+
* }
|
|
183
|
+
*
|
|
184
|
+
* function BookView({doc}: BookViewProps) {
|
|
185
|
+
* const {data: book} = useDocument<Book>({...doc})
|
|
186
|
+
* return <h1>{book?.title ?? 'Untitled'} by {book?.author ?? 'Unknown'}</h1>
|
|
187
|
+
* }
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* @example Fetching a specific field with explicit type
|
|
132
191
|
* ```tsx
|
|
133
192
|
* import {useDocument, type DocumentHandle} from '@sanity/sdk-react'
|
|
134
193
|
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
194
|
+
* interface BookTitleProps {
|
|
195
|
+
* doc: DocumentHandle
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* function BookTitle({doc}: BookTitleProps) {
|
|
199
|
+
* const {data: title} = useDocument<string>({...doc, path: 'title'})
|
|
200
|
+
* return <h1>{title ?? 'Untitled'}</h1>
|
|
138
201
|
* }
|
|
139
202
|
* ```
|
|
140
203
|
*
|
|
141
204
|
* @inlineType DocumentOptions
|
|
142
205
|
*/
|
|
143
|
-
|
|
206
|
+
<TData, TPath extends string>(
|
|
207
|
+
options: UseDocumentOptions<TPath>,
|
|
208
|
+
): TPath extends string ? {data: TData | undefined} : {data: TData | null}
|
|
144
209
|
|
|
145
210
|
/**
|
|
146
211
|
* @internal
|
|
@@ -152,11 +217,10 @@ interface UseDocument {
|
|
|
152
217
|
* @public
|
|
153
218
|
* Reads and subscribes to a document's realtime state, incorporating both local and remote changes.
|
|
154
219
|
*
|
|
155
|
-
*
|
|
156
|
-
* - `useDocument<MyType>({...})` returns `{data: MyType | null}`
|
|
157
|
-
* - `useDocument<string>({..., path: 'title'})` returns `{data: string | undefined}`
|
|
220
|
+
* This hook comes in two main flavors to suit your needs:
|
|
158
221
|
*
|
|
159
|
-
*
|
|
222
|
+
* 1. **[Type Inference](#usedocument-via-type-inference-recommended)** (Recommended) - Automatically gets types from your Sanity schema
|
|
223
|
+
* 2. **[Explicit Types](#usedocument-via-explicit-types)** - Manually specify types when needed
|
|
160
224
|
*
|
|
161
225
|
* @remarks
|
|
162
226
|
* `useDocument` is ideal for realtime editing interfaces where you need immediate feedback on changes.
|
|
@@ -173,6 +237,7 @@ interface UseDocument {
|
|
|
173
237
|
* @function
|
|
174
238
|
*/
|
|
175
239
|
export const useDocument = wrapHookWithData((options: UseDocumentOptions) => {
|
|
240
|
+
useTrackHookUsage('useDocument')
|
|
176
241
|
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
177
242
|
return useDocumentValue(normalizedOptions)
|
|
178
243
|
}) as UseDocument
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// tests/useDocumentEvent.test.ts
|
|
2
2
|
import {type DocumentEvent, type DocumentHandle, subscribeDocumentEvents} from '@sanity/sdk'
|
|
3
|
+
import {renderHook} from '@testing-library/react'
|
|
3
4
|
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
5
|
|
|
5
|
-
import {
|
|
6
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
6
7
|
import {useDocumentEvent} from './useDocumentEvent'
|
|
7
8
|
|
|
8
9
|
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
@@ -13,7 +14,6 @@ vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
|
13
14
|
const docHandle: DocumentHandle = {
|
|
14
15
|
documentId: 'doc1',
|
|
15
16
|
documentType: 'book',
|
|
16
|
-
resource: {projectId: 'p', dataset: 'd'},
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
describe('useDocumentEvent hook', () => {
|
|
@@ -26,7 +26,13 @@ describe('useDocumentEvent hook', () => {
|
|
|
26
26
|
const unsubscribe = vi.fn()
|
|
27
27
|
vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
|
|
28
28
|
|
|
29
|
-
renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent})
|
|
29
|
+
renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent}), {
|
|
30
|
+
wrapper: ({children}) => (
|
|
31
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
32
|
+
{children}
|
|
33
|
+
</ResourceProvider>
|
|
34
|
+
),
|
|
35
|
+
})
|
|
30
36
|
|
|
31
37
|
expect(vi.mocked(subscribeDocumentEvents)).toHaveBeenCalledTimes(1)
|
|
32
38
|
expect(vi.mocked(subscribeDocumentEvents).mock.calls[0][0]).toEqual(expect.any(Object))
|
|
@@ -44,7 +50,13 @@ describe('useDocumentEvent hook', () => {
|
|
|
44
50
|
const unsubscribe = vi.fn()
|
|
45
51
|
vi.mocked(subscribeDocumentEvents).mockReturnValue(unsubscribe)
|
|
46
52
|
|
|
47
|
-
const {unmount} = renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent})
|
|
53
|
+
const {unmount} = renderHook(() => useDocumentEvent({...docHandle, onEvent: handleEvent}), {
|
|
54
|
+
wrapper: ({children}) => (
|
|
55
|
+
<ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
|
|
56
|
+
{children}
|
|
57
|
+
</ResourceProvider>
|
|
58
|
+
),
|
|
59
|
+
})
|
|
48
60
|
unmount()
|
|
49
61
|
expect(unsubscribe).toHaveBeenCalledTimes(1)
|
|
50
62
|
})
|
|
@@ -4,6 +4,7 @@ import {useCallback, useEffect, useInsertionEffect, useRef} from 'react'
|
|
|
4
4
|
import {type ResourceHandle} from '../../config/handles'
|
|
5
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
6
|
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
7
|
+
import {useTrackHookUsage} from '../helpers/useTrackHookUsage'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @public
|
|
@@ -11,7 +12,7 @@ import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOpti
|
|
|
11
12
|
export interface UseDocumentEventOptions<
|
|
12
13
|
TDataset extends string = string,
|
|
13
14
|
TProjectId extends string = string,
|
|
14
|
-
> extends ResourceHandle<
|
|
15
|
+
> extends ResourceHandle<TDataset, TProjectId> {
|
|
15
16
|
onEvent: (documentEvent: DocumentEvent) => void
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -22,19 +23,19 @@ export interface UseDocumentEventOptions<
|
|
|
22
23
|
* Subscribes an event handler to events in your application's document store.
|
|
23
24
|
*
|
|
24
25
|
* @category Documents
|
|
25
|
-
* @param options - An object containing the event handler (`onEvent`) and optionally a `
|
|
26
|
+
* @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.
|
|
26
27
|
* @example Creating a custom hook for document event toasts
|
|
27
28
|
* ```tsx
|
|
28
|
-
* import {type
|
|
29
|
+
* import {createDatasetHandle, type DatasetHandle, type DocumentEvent, useDocumentEvent} from '@sanity/sdk-react'
|
|
29
30
|
* import {useToast} from './my-ui-library'
|
|
30
31
|
*
|
|
31
|
-
* // Define options for the custom hook
|
|
32
|
-
* interface DocumentToastsOptions {
|
|
33
|
-
*
|
|
32
|
+
* // Define options for the custom hook, extending DatasetHandle
|
|
33
|
+
* interface DocumentToastsOptions extends DatasetHandle {
|
|
34
|
+
* // Could add more options, e.g., { includeEvents: DocumentEvent['type'][] }
|
|
34
35
|
* }
|
|
35
36
|
*
|
|
36
37
|
* // Define the custom hook
|
|
37
|
-
* function useDocumentToasts({
|
|
38
|
+
* function useDocumentToasts({...datasetHandle}: DocumentToastsOptions = {}) {
|
|
38
39
|
* const showToast = useToast() // Get the toast function
|
|
39
40
|
*
|
|
40
41
|
* // Define the event handler logic to show toasts on specific events
|
|
@@ -46,20 +47,24 @@ export interface UseDocumentEventOptions<
|
|
|
46
47
|
* } else if (event.type === 'deleted') {
|
|
47
48
|
* showToast(`Document ${event.documentId} deleted.`)
|
|
48
49
|
* } else {
|
|
50
|
+
* // Optionally log other events for debugging
|
|
49
51
|
* console.log('Document Event:', event.type, event.documentId)
|
|
50
52
|
* }
|
|
51
53
|
* }
|
|
52
54
|
*
|
|
55
|
+
* // Call the original hook, spreading the handle properties
|
|
53
56
|
* useDocumentEvent({
|
|
54
|
-
*
|
|
57
|
+
* ...datasetHandle, // Spread the dataset handle (projectId, dataset)
|
|
55
58
|
* onEvent: handleEvent,
|
|
56
59
|
* })
|
|
57
60
|
* }
|
|
58
61
|
*
|
|
59
62
|
* function MyComponentWithToasts() {
|
|
60
|
-
*
|
|
63
|
+
* // Use the custom hook, passing specific handle info
|
|
64
|
+
* const specificHandle = createDatasetHandle({ projectId: 'p1', dataset: 'ds1' })
|
|
65
|
+
* useDocumentToasts(specificHandle)
|
|
61
66
|
*
|
|
62
|
-
* // Or
|
|
67
|
+
* // // Or use it relying on context for the handle
|
|
63
68
|
* // useDocumentToasts()
|
|
64
69
|
*
|
|
65
70
|
* return <div>...</div>
|
|
@@ -73,6 +78,7 @@ export function useDocumentEvent<
|
|
|
73
78
|
// Single options object parameter
|
|
74
79
|
options: UseDocumentEventOptions<TDataset, TProjectId>,
|
|
75
80
|
): void {
|
|
81
|
+
useTrackHookUsage('useDocumentEvent')
|
|
76
82
|
// Destructure handler and datasetHandle from options
|
|
77
83
|
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
78
84
|
const {onEvent, ...datasetHandle} = normalizedOptions
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
|
|
2
|
+
import {renderHook as reactRenderHook} from '@testing-library/react'
|
|
2
3
|
import {BehaviorSubject, firstValueFrom, Observable} from 'rxjs'
|
|
3
|
-
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
|
|
4
5
|
|
|
5
6
|
import {act, renderHook, waitFor} from '../../../test/test-utils'
|
|
6
7
|
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
@@ -46,8 +47,8 @@ describe('usePermissions', () => {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
let permissionsSubject: BehaviorSubject<DocumentPermissionsResult | undefined>
|
|
49
|
-
let mockSubscribe:
|
|
50
|
-
let mockGetCurrent:
|
|
50
|
+
let mockSubscribe: Mock<(onStoreChanged?: () => void) => () => void>
|
|
51
|
+
let mockGetCurrent: Mock<() => DocumentPermissionsResult | undefined>
|
|
51
52
|
|
|
52
53
|
beforeEach(() => {
|
|
53
54
|
vi.clearAllMocks()
|
|
@@ -71,7 +72,7 @@ describe('usePermissions', () => {
|
|
|
71
72
|
observable:
|
|
72
73
|
permissionsSubject.asObservable() as unknown as Observable<DocumentPermissionsResult>,
|
|
73
74
|
subscribe: mockSubscribe,
|
|
74
|
-
getCurrent: mockGetCurrent,
|
|
75
|
+
getCurrent: mockGetCurrent as unknown as () => DocumentPermissionsResult,
|
|
75
76
|
})
|
|
76
77
|
})
|
|
77
78
|
|
|
@@ -177,7 +178,7 @@ describe('usePermissions', () => {
|
|
|
177
178
|
vi.mocked(firstValueFrom).mockReturnValueOnce(mockPromise)
|
|
178
179
|
|
|
179
180
|
// This should throw the promise and suspend
|
|
180
|
-
const {result} =
|
|
181
|
+
const {result} = reactRenderHook(
|
|
181
182
|
() => {
|
|
182
183
|
try {
|
|
183
184
|
return useDocumentPermissions(mockAction)
|
|
@@ -216,7 +217,7 @@ describe('usePermissions', () => {
|
|
|
216
217
|
it('throws when no resource is found from action or context', () => {
|
|
217
218
|
// Provide SanityInstance via ResourceProvider but no resource, so contextResource is undefined
|
|
218
219
|
expect(() => {
|
|
219
|
-
|
|
220
|
+
reactRenderHook(
|
|
220
221
|
() =>
|
|
221
222
|
useDocumentPermissions({
|
|
222
223
|
type: 'document.publish',
|
|
@@ -228,7 +229,7 @@ describe('usePermissions', () => {
|
|
|
228
229
|
wrapper: ({children}) => <ResourceProvider fallback={null}>{children}</ResourceProvider>,
|
|
229
230
|
},
|
|
230
231
|
)
|
|
231
|
-
}).toThrow(/resource
|
|
232
|
+
}).toThrow(/No resource found/)
|
|
232
233
|
})
|
|
233
234
|
|
|
234
235
|
it('should react to permission state changes', async () => {
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import {type DocumentAction, type DocumentPermissionsResult, getPermissionsState} from '@sanity/sdk'
|
|
2
|
+
import {isDeepEqual} from '@sanity/sdk/_internal'
|
|
2
3
|
import {useCallback, useContext, useMemo, useSyncExternalStore} from 'react'
|
|
3
4
|
import {filter, firstValueFrom} from 'rxjs'
|
|
4
5
|
|
|
5
|
-
import {ResourceContext} from '../../context/DefaultResourceContext'
|
|
6
6
|
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
7
7
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
normalizeResourceOptions,
|
|
10
|
+
useEffectiveContextResource,
|
|
11
|
+
type WithResourceNameSupport,
|
|
12
|
+
} from '../helpers/useNormalizedResourceOptions'
|
|
13
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
14
|
+
|
|
15
|
+
const noopSubscribe = () => () => {}
|
|
16
|
+
const returnUndefined = () => undefined
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
*
|
|
@@ -85,63 +93,87 @@ import {normalizeResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
|
85
93
|
* ```
|
|
86
94
|
*/
|
|
87
95
|
export function useDocumentPermissions(
|
|
88
|
-
actionOrActions:
|
|
96
|
+
actionOrActions:
|
|
97
|
+
| WithResourceNameSupport<DocumentAction>
|
|
98
|
+
| WithResourceNameSupport<DocumentAction>[],
|
|
89
99
|
): DocumentPermissionsResult {
|
|
90
100
|
const instance = useSanityInstance()
|
|
91
|
-
|
|
101
|
+
trackHookUsage(instance, 'useDocumentPermissions')
|
|
102
|
+
const effectiveContextResource = useEffectiveContextResource()
|
|
92
103
|
const resources = useContext(ResourcesContext)
|
|
93
104
|
|
|
94
|
-
const
|
|
95
|
-
|
|
105
|
+
const {
|
|
106
|
+
actions: normalizedActions,
|
|
107
|
+
resource: actionResource,
|
|
108
|
+
error: validationError,
|
|
109
|
+
} = useMemo(() => {
|
|
110
|
+
const normalized = Array.isArray(actionOrActions)
|
|
96
111
|
? actionOrActions.map((action) =>
|
|
97
|
-
normalizeResourceOptions(action, resources,
|
|
112
|
+
normalizeResourceOptions(action, resources, effectiveContextResource),
|
|
98
113
|
)
|
|
99
|
-
: [normalizeResourceOptions(actionOrActions, resources,
|
|
100
|
-
}, [actionOrActions, resources, contextResource])
|
|
101
|
-
|
|
102
|
-
// if actions is an array, we need to check that all actions belong to the same resource
|
|
103
|
-
let resource
|
|
114
|
+
: [normalizeResourceOptions(actionOrActions, resources, effectiveContextResource)]
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
let resource
|
|
117
|
+
for (const action of normalized) {
|
|
118
|
+
if (action.resource) {
|
|
119
|
+
if (!resource) resource = action.resource
|
|
120
|
+
if (!isDeepEqual(action.resource, resource)) {
|
|
121
|
+
return {
|
|
122
|
+
actions: normalized,
|
|
123
|
+
resource,
|
|
124
|
+
error: new Error(
|
|
125
|
+
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(action.resource)}" but expected "${JSON.stringify(resource)}".`,
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
130
|
}
|
|
114
|
-
|
|
131
|
+
return {actions: normalized, resource, error: undefined}
|
|
132
|
+
}, [actionOrActions, resources, effectiveContextResource])
|
|
115
133
|
|
|
116
|
-
const effectiveResource =
|
|
117
|
-
|
|
118
|
-
if (!effectiveResource) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
'No resource found. Provide a resource via the action handle or wrap with a resource context.',
|
|
121
|
-
)
|
|
122
|
-
}
|
|
134
|
+
const effectiveResource = actionResource ?? effectiveContextResource
|
|
123
135
|
|
|
136
|
+
// Keep hooks unconditional — validation errors and missing-resource errors are
|
|
137
|
+
// thrown after all hooks so that the hook call count stays stable across renders.
|
|
124
138
|
const permissionsOptions = useMemo(
|
|
125
|
-
() =>
|
|
139
|
+
() =>
|
|
140
|
+
effectiveResource
|
|
141
|
+
? {
|
|
142
|
+
resource: effectiveResource,
|
|
143
|
+
// `Omit<>` on `DocumentAction` loses the discriminant; runtime values are still actions.
|
|
144
|
+
actions: normalizedActions as DocumentAction[],
|
|
145
|
+
}
|
|
146
|
+
: undefined,
|
|
126
147
|
[effectiveResource, normalizedActions],
|
|
127
148
|
)
|
|
128
149
|
|
|
129
|
-
const
|
|
130
|
-
() => getPermissionsState(instance, permissionsOptions)
|
|
150
|
+
const stateSource = useMemo(
|
|
151
|
+
() => (permissionsOptions ? getPermissionsState(instance, permissionsOptions) : undefined),
|
|
131
152
|
[permissionsOptions, instance],
|
|
132
153
|
)
|
|
154
|
+
|
|
155
|
+
const isDocumentReady = useCallback(
|
|
156
|
+
() => stateSource !== undefined && stateSource.getCurrent() !== undefined,
|
|
157
|
+
[stateSource],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const result = useSyncExternalStore(
|
|
161
|
+
stateSource?.subscribe ?? noopSubscribe,
|
|
162
|
+
stateSource?.getCurrent ?? returnUndefined,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// All hooks have been called — safe to throw now.
|
|
166
|
+
if (validationError) throw validationError
|
|
167
|
+
if (!effectiveResource) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
'No resource found. Provide a resource via the action handle or wrap with a resource context.',
|
|
170
|
+
)
|
|
171
|
+
}
|
|
133
172
|
if (!isDocumentReady()) {
|
|
134
173
|
throw firstValueFrom(
|
|
135
|
-
|
|
136
|
-
filter((result) => result !== undefined),
|
|
137
|
-
),
|
|
174
|
+
stateSource!.observable.pipe(filter((permissions) => permissions !== undefined)),
|
|
138
175
|
)
|
|
139
176
|
}
|
|
140
177
|
|
|
141
|
-
|
|
142
|
-
() => getPermissionsState(instance, permissionsOptions),
|
|
143
|
-
[permissionsOptions, instance],
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
return useSyncExternalStore(subscribe, getCurrent) as DocumentPermissionsResult
|
|
178
|
+
return result as DocumentPermissionsResult
|
|
147
179
|
}
|