@sanity/sdk-react 3.0.0-rc.0 → 3.0.0-rc.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 +103 -155
- package/dist/index.d.ts +1564 -682
- package/dist/index.js +1118 -612
- package/dist/index.js.map +1 -1
- package/package.json +51 -54
- package/src/_exports/index.ts +2 -4
- package/src/_exports/sdk-react.ts +26 -17
- 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 +94 -87
- package/src/components/auth/AuthBoundary.recovery.test.tsx +86 -0
- package/src/components/auth/AuthBoundary.test.tsx +26 -8
- package/src/components/auth/AuthBoundary.tsx +28 -6
- package/src/components/auth/DashboardAccessRequest.tsx +37 -0
- package/src/components/auth/LoginError.test.tsx +191 -5
- package/src/components/auth/LoginError.tsx +103 -59
- 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/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 +72 -90
- package/src/context/ResourceProvider.tsx +59 -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 +1 -1
- package/src/hooks/agent/agentActions.ts +51 -71
- 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/useCreateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useCreateUserApplication.ts +11 -0
- package/src/hooks/applications/useDeleteApplication.test-d.ts +12 -0
- package/src/hooks/applications/useDeleteApplication.ts +11 -0
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useDeleteUserApplication.ts +11 -0
- package/src/hooks/applications/useUpdateApplication.test-d.ts +12 -0
- package/src/hooks/applications/useUpdateApplication.ts +11 -0
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +14 -0
- package/src/hooks/applications/useUpdateUserApplication.ts +11 -0
- package/src/hooks/applications/useUserApplication.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplication.ts +14 -0
- package/src/hooks/applications/useUserApplications.test-d.ts +11 -0
- package/src/hooks/applications/useUserApplications.ts +14 -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/context/useResource.test.tsx +32 -0
- package/src/hooks/context/useResource.ts +24 -0
- package/src/hooks/context/useSanityInstance.test.tsx +72 -3
- package/src/hooks/context/useSanityInstance.ts +30 -8
- package/src/hooks/dashboard/useManageFavorite.test.tsx +110 -46
- package/src/hooks/dashboard/useManageFavorite.ts +10 -16
- 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 +1 -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 +36 -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 +253 -0
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +92 -50
- 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 +40 -22
- package/src/hooks/presence/usePresence.test.tsx +0 -15
- package/src/hooks/presence/usePresence.ts +9 -20
- package/src/hooks/preview/useDocumentPreview.test.tsx +0 -1
- package/src/hooks/preview/useDocumentPreview.tsx +10 -8
- 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 +19 -26
- package/src/hooks/query/useQuery.test.tsx +6 -5
- package/src/hooks/query/useQuery.ts +91 -9
- 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 +2 -0
- package/src/hooks/users/useUsers.test.tsx +103 -7
- package/src/hooks/users/useUsers.ts +37 -3
- 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,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
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type DocumentHandle
|
|
2
|
+
type DocumentHandle,
|
|
3
|
+
type DocumentOptions,
|
|
3
4
|
getDocumentSyncStatus,
|
|
4
5
|
resolveDocument,
|
|
5
6
|
type SanityInstance,
|
|
6
7
|
type StateSource,
|
|
7
8
|
} from '@sanity/sdk'
|
|
9
|
+
import {identity} from 'rxjs'
|
|
8
10
|
|
|
9
|
-
import {type DocumentHandle} from '../../config/handles'
|
|
10
11
|
import {createStateSourceHook} from '../helpers/createStateSourceHook'
|
|
11
12
|
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
12
13
|
|
|
@@ -15,9 +16,9 @@ type UseDocumentSyncStatus = {
|
|
|
15
16
|
* Exposes the document's sync status between local and remote document states.
|
|
16
17
|
*
|
|
17
18
|
* @category Documents
|
|
18
|
-
* @param doc - The document handle to get sync status for. If you pass a `
|
|
19
|
-
* the document will be read from the specified
|
|
20
|
-
* the
|
|
19
|
+
* @param doc - The document handle to get sync status for. If you pass a `DocumentHandle` with specified `projectId` and `dataset`,
|
|
20
|
+
* 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,
|
|
21
|
+
* the document will use the nearest instance from context.
|
|
21
22
|
* @returns `true` if local changes are synced with remote, `false` if changes are pending. Note: Suspense handles loading states.
|
|
22
23
|
* @example Show sync status indicator
|
|
23
24
|
* ```tsx
|
|
@@ -49,18 +50,21 @@ type UseDocumentSyncStatus = {
|
|
|
49
50
|
const useDocumentSyncStatusValue = createStateSourceHook({
|
|
50
51
|
getState: getDocumentSyncStatus as (
|
|
51
52
|
instance: SanityInstance,
|
|
52
|
-
doc:
|
|
53
|
+
doc: DocumentHandle,
|
|
53
54
|
) => StateSource<boolean>,
|
|
54
|
-
shouldSuspend: (instance, doc:
|
|
55
|
+
shouldSuspend: (instance, doc: DocumentHandle) =>
|
|
55
56
|
getDocumentSyncStatus(instance, doc).getCurrent() === undefined,
|
|
56
|
-
suspender: (instance, doc:
|
|
57
|
+
suspender: (instance, doc: DocumentHandle) => resolveDocument(instance, doc),
|
|
58
|
+
getConfig: identity,
|
|
57
59
|
})
|
|
58
60
|
|
|
59
61
|
/**
|
|
60
62
|
* @public
|
|
61
63
|
* @function
|
|
62
64
|
*/
|
|
63
|
-
export const useDocumentSyncStatus: UseDocumentSyncStatus = (
|
|
65
|
+
export const useDocumentSyncStatus: UseDocumentSyncStatus = (
|
|
66
|
+
options: DocumentOptions<string | undefined>,
|
|
67
|
+
) => {
|
|
64
68
|
const normalizedOptions = useNormalizedResourceOptions(options)
|
|
65
69
|
return useDocumentSyncStatusValue(normalizedOptions)
|
|
66
70
|
}
|
|
@@ -39,12 +39,11 @@ const doc = {
|
|
|
39
39
|
const docHandle = createDocumentHandle({
|
|
40
40
|
documentId: 'doc1',
|
|
41
41
|
documentType: 'book',
|
|
42
|
-
resource: {projectId: 'test-project', dataset: 'test-dataset'},
|
|
43
42
|
})
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
resource: {projectId: 'test
|
|
43
|
+
|
|
44
|
+
const normalizedDoc = {
|
|
45
|
+
...docHandle,
|
|
46
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
// Define a single generic TestDocument type
|
|
@@ -55,6 +54,16 @@ interface Book extends SanityDocument {
|
|
|
55
54
|
title?: string
|
|
56
55
|
}
|
|
57
56
|
|
|
57
|
+
// Scope the TestDocument type to the project/datasets used in tests
|
|
58
|
+
type AllTestSchemaTypes = Book
|
|
59
|
+
|
|
60
|
+
// Augment the 'groq' module
|
|
61
|
+
declare module 'groq' {
|
|
62
|
+
interface SanitySchemas {
|
|
63
|
+
'default:default': AllTestSchemaTypes
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
58
67
|
describe('useEditDocument hook', () => {
|
|
59
68
|
beforeEach(() => {
|
|
60
69
|
vi.clearAllMocks()
|
|
@@ -73,8 +82,8 @@ describe('useEditDocument hook', () => {
|
|
|
73
82
|
|
|
74
83
|
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
|
|
75
84
|
const promise = result.current('newValue')
|
|
76
|
-
expect(editDocument).toHaveBeenCalledWith(
|
|
77
|
-
expect(apply).toHaveBeenCalledWith(editDocument(
|
|
85
|
+
expect(editDocument).toHaveBeenCalledWith(normalizedDoc, {set: {foo: 'newValue'}})
|
|
86
|
+
expect(apply).toHaveBeenCalledWith(editDocument(normalizedDoc, {set: {foo: 'newValue'}}))
|
|
78
87
|
const actionsResult = await promise
|
|
79
88
|
expect(actionsResult).toEqual({transactionId: 'tx1'})
|
|
80
89
|
})
|
|
@@ -94,7 +103,7 @@ describe('useEditDocument hook', () => {
|
|
|
94
103
|
|
|
95
104
|
const {result} = renderHook(() => useEditDocument(docHandle))
|
|
96
105
|
const promise = result.current({...doc, foo: 'baz', extra: 'old', _id: 'doc1'})
|
|
97
|
-
expect(apply).toHaveBeenCalledWith([editDocument(
|
|
106
|
+
expect(apply).toHaveBeenCalledWith([editDocument(normalizedDoc, {set: {foo: 'baz'}})])
|
|
98
107
|
const actionsResult = await promise
|
|
99
108
|
expect(actionsResult).toEqual({transactionId: 'tx2'})
|
|
100
109
|
})
|
|
@@ -112,10 +121,8 @@ describe('useEditDocument hook', () => {
|
|
|
112
121
|
|
|
113
122
|
const {result} = renderHook(() => useEditDocument<string>({...docHandle, path: 'foo'}))
|
|
114
123
|
const promise = result.current((prev: unknown) => `${prev}Updated`) // 'bar' becomes 'barUpdated'
|
|
115
|
-
expect(editDocument).toHaveBeenCalledWith(
|
|
116
|
-
expect(apply).toHaveBeenCalledWith(
|
|
117
|
-
editDocument(normalizedDocHandle, {set: {foo: 'barUpdated'}}),
|
|
118
|
-
)
|
|
124
|
+
expect(editDocument).toHaveBeenCalledWith(normalizedDoc, {set: {foo: 'barUpdated'}})
|
|
125
|
+
expect(apply).toHaveBeenCalledWith(editDocument(normalizedDoc, {set: {foo: 'barUpdated'}}))
|
|
119
126
|
const actionsResult = await promise
|
|
120
127
|
expect(actionsResult).toEqual({transactionId: 'tx3'})
|
|
121
128
|
})
|
|
@@ -133,8 +140,8 @@ describe('useEditDocument hook', () => {
|
|
|
133
140
|
vi.mocked(useApplyDocumentActions).mockReturnValue(apply)
|
|
134
141
|
|
|
135
142
|
const {result} = renderHook(() => useEditDocument(docHandle))
|
|
136
|
-
const promise = result.current((prevDoc:
|
|
137
|
-
expect(apply).toHaveBeenCalledWith([editDocument(
|
|
143
|
+
const promise = result.current((prevDoc: Book) => ({...prevDoc, foo: 'baz'}))
|
|
144
|
+
expect(apply).toHaveBeenCalledWith([editDocument(normalizedDoc, {set: {foo: 'baz'}})])
|
|
138
145
|
const actionsResult = await promise
|
|
139
146
|
expect(actionsResult).toEqual({transactionId: 'tx4'})
|
|
140
147
|
})
|
|
@@ -1,44 +1,86 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ActionsResult,
|
|
3
|
+
type DocumentOptions,
|
|
4
|
+
editDocument,
|
|
5
|
+
getDocumentState,
|
|
6
|
+
type JsonMatch,
|
|
7
|
+
resolveDocument,
|
|
8
|
+
} from '@sanity/sdk'
|
|
9
|
+
import {type SanityDocument} from 'groq'
|
|
2
10
|
import {useCallback} from 'react'
|
|
3
11
|
|
|
4
|
-
import {type DocumentHandle} from '../../config/handles'
|
|
5
12
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
13
|
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
14
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
7
15
|
import {useApplyDocumentActions} from './useApplyDocumentActions'
|
|
8
16
|
|
|
9
17
|
const ignoredKeys = ['_id', '_type', '_createdAt', '_updatedAt', '_rev']
|
|
10
18
|
|
|
11
19
|
type Updater<TValue> = TValue | ((currentValue: TValue) => TValue)
|
|
12
20
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
// Overload 1: No path, relies on Typegen
|
|
22
|
+
/**
|
|
23
|
+
* @public
|
|
24
|
+
* Edit an entire document, relying on Typegen for the type.
|
|
25
|
+
*
|
|
26
|
+
* @param options - Document options including `documentId`, `documentType`, and optionally `projectId`/`dataset`.
|
|
27
|
+
* @returns A stable function to update the document state. Accepts either the new document state or an updater function `(currentValue) => nextValue`.
|
|
28
|
+
* Returns a promise resolving to the {@link ActionsResult}.
|
|
29
|
+
*/
|
|
30
|
+
export function useEditDocument<
|
|
31
|
+
TDocumentType extends string = string,
|
|
32
|
+
TDataset extends string = string,
|
|
33
|
+
TProjectId extends string = string,
|
|
34
|
+
>(
|
|
35
|
+
options: DocumentOptions<undefined, TDocumentType, TDataset, TProjectId>,
|
|
36
|
+
): (
|
|
37
|
+
nextValue: Updater<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>,
|
|
38
|
+
) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
|
|
39
|
+
|
|
40
|
+
// Overload 2: Path provided, relies on Typegen
|
|
41
|
+
/**
|
|
42
|
+
* @public
|
|
43
|
+
* Edit a specific path within a document, relying on Typegen for the type.
|
|
44
|
+
*
|
|
45
|
+
* @param options - Document options including `documentId`, `documentType`, `path`, and optionally `projectId`/`dataset`.
|
|
46
|
+
* @returns A stable function to update the value at the specified path. Accepts either the new value or an updater function `(currentValue) => nextValue`.
|
|
47
|
+
* Returns a promise resolving to the {@link ActionsResult}.
|
|
48
|
+
*/
|
|
49
|
+
export function useEditDocument<
|
|
50
|
+
TPath extends string = string,
|
|
51
|
+
TDocumentType extends string = string,
|
|
52
|
+
TDataset extends string = string,
|
|
53
|
+
TProjectId extends string = string,
|
|
54
|
+
>(
|
|
55
|
+
options: DocumentOptions<TPath, TDocumentType, TDataset, TProjectId>,
|
|
56
|
+
): (
|
|
57
|
+
nextValue: Updater<JsonMatch<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>, TPath>>,
|
|
58
|
+
) => Promise<ActionsResult<SanityDocument<TDocumentType, `${TProjectId}.${TDataset}`>>>
|
|
17
59
|
|
|
18
|
-
// Overload
|
|
60
|
+
// Overload 3: Explicit type, no path
|
|
19
61
|
/**
|
|
20
62
|
* @public
|
|
21
63
|
* Edit an entire document with an explicit type `TData`.
|
|
22
64
|
*
|
|
23
|
-
* @param options - Document options including `documentId` and optionally `
|
|
65
|
+
* @param options - Document options including `documentId` and optionally `projectId`/`dataset`.
|
|
24
66
|
* @returns A stable function to update the document state. Accepts either the new document state (`TData`) or an updater function `(currentValue: TData) => nextValue: TData`.
|
|
25
67
|
* Returns a promise resolving to the {@link ActionsResult}.
|
|
26
68
|
*/
|
|
27
69
|
export function useEditDocument<TData>(
|
|
28
|
-
options:
|
|
70
|
+
options: DocumentOptions<undefined>,
|
|
29
71
|
): (nextValue: Updater<TData>) => Promise<ActionsResult>
|
|
30
72
|
|
|
31
|
-
// Overload
|
|
73
|
+
// Overload 4: Explicit type, path provided
|
|
32
74
|
/**
|
|
33
75
|
* @public
|
|
34
76
|
* Edit a specific path within a document with an explicit type `TData`.
|
|
35
77
|
*
|
|
36
|
-
* @param options - Document options including `documentId`, `path`, and optionally `
|
|
78
|
+
* @param options - Document options including `documentId`, `path`, and optionally `projectId`/`dataset`.
|
|
37
79
|
* @returns A stable function to update the value at the specified path. Accepts either the new value (`TData`) or an updater function `(currentValue: TData) => nextValue: TData`.
|
|
38
80
|
* Returns a promise resolving to the {@link ActionsResult}.
|
|
39
81
|
*/
|
|
40
82
|
export function useEditDocument<TData>(
|
|
41
|
-
options:
|
|
83
|
+
options: DocumentOptions<string>,
|
|
42
84
|
): (nextValue: Updater<TData>) => Promise<ActionsResult>
|
|
43
85
|
|
|
44
86
|
/**
|
|
@@ -54,9 +96,11 @@ export function useEditDocument<TData>(
|
|
|
54
96
|
* - Integrating with the active {@link SanityInstance} context.
|
|
55
97
|
* - Utilizing `useApplyDocumentActions` internally for optimistic updates and transaction handling.
|
|
56
98
|
*
|
|
57
|
-
* It offers overloads for flexibility:
|
|
58
|
-
* 1. **
|
|
59
|
-
* 2. **
|
|
99
|
+
* It offers several overloads for flexibility:
|
|
100
|
+
* 1. **Typegen (Full Document):** Edit the entire document, inferring types from your schema.
|
|
101
|
+
* 2. **Typegen (Specific Path):** Edit a specific field, inferring types.
|
|
102
|
+
* 3. **Explicit Type (Full Document):** Edit the entire document with a manually specified type.
|
|
103
|
+
* 4. **Explicit Type (Specific Path):** Edit a specific field with a manually specified type.
|
|
60
104
|
*
|
|
61
105
|
* **LiveEdit Documents:**
|
|
62
106
|
* For documents using {@link DocumentHandle.liveEdit | liveEdit mode} (set via `liveEdit: true` in the document handle), edits are applied directly to the published document without creating a draft.
|
|
@@ -64,7 +108,87 @@ export function useEditDocument<TData>(
|
|
|
64
108
|
* This hook relies on the document state being loaded. If the document is not yet available
|
|
65
109
|
* (e.g., during initial load), the component using this hook will suspend.
|
|
66
110
|
*
|
|
67
|
-
* @example Basic Usage
|
|
111
|
+
* @example Basic Usage (Typegen, Full Document)
|
|
112
|
+
* ```tsx
|
|
113
|
+
* import {useCallback} from 'react';
|
|
114
|
+
* import {useEditDocument, useDocument, type DocumentHandle} from '@sanity/sdk-react'
|
|
115
|
+
*
|
|
116
|
+
* // Assume 'product' schema has a 'title' field (string)
|
|
117
|
+
* interface ProductEditorProps {
|
|
118
|
+
* productHandle: DocumentHandle<'product'> // Typegen infers 'product' type
|
|
119
|
+
* }
|
|
120
|
+
*
|
|
121
|
+
* function ProductEditor({ productHandle }: ProductEditorProps) {
|
|
122
|
+
* // Fetch the document to display its current state (optional)
|
|
123
|
+
* const {data: product} = useDocument(productHandle);
|
|
124
|
+
* // Get the edit function for the full document
|
|
125
|
+
* const editProduct = useEditDocument(productHandle);
|
|
126
|
+
*
|
|
127
|
+
* // Use useCallback for stable event handlers
|
|
128
|
+
* const handleTitleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
129
|
+
* const newTitle = event.target.value;
|
|
130
|
+
* // Use the functional updater for safe partial updates
|
|
131
|
+
* editProduct(prev => ({
|
|
132
|
+
* ...prev,
|
|
133
|
+
* title: newTitle,
|
|
134
|
+
* })).
|
|
135
|
+
* }, [editProduct]);
|
|
136
|
+
*
|
|
137
|
+
* return (
|
|
138
|
+
* <div>
|
|
139
|
+
* <label>
|
|
140
|
+
* Product Title:
|
|
141
|
+
* <input
|
|
142
|
+
* type="text"
|
|
143
|
+
* value={product?.title ?? ''}
|
|
144
|
+
* onChange={handleTitleChange}
|
|
145
|
+
* />
|
|
146
|
+
* </label>
|
|
147
|
+
* </div>
|
|
148
|
+
* );
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example Editing a Specific Path (Typegen)
|
|
153
|
+
* ```tsx
|
|
154
|
+
* import React, { useCallback } from 'react';
|
|
155
|
+
* import {useEditDocument, useDocument, type DocumentHandle, type DocumentOptions} from '@sanity/sdk-react'
|
|
156
|
+
*
|
|
157
|
+
* // Assume 'product' schema has a 'price' field (number)
|
|
158
|
+
* interface ProductPriceEditorProps {
|
|
159
|
+
* productHandle: DocumentHandle<'product'>;
|
|
160
|
+
* }
|
|
161
|
+
*
|
|
162
|
+
* function ProductPriceEditor({ productHandle }: ProductPriceEditorProps) {
|
|
163
|
+
* // Construct DocumentOptions internally, combining the handle and a hardcoded path
|
|
164
|
+
* const priceOptions {
|
|
165
|
+
* ...productHandle,
|
|
166
|
+
* path: 'price', // Hardcode the path to edit
|
|
167
|
+
* };
|
|
168
|
+
*
|
|
169
|
+
* // Fetch the current price to display it
|
|
170
|
+
* const {data: currentPrice} = useDocument(priceOptions);
|
|
171
|
+
* // Get the edit function for the specific path 'price'
|
|
172
|
+
* const editPrice = useEditDocument(priceOptions);
|
|
173
|
+
*
|
|
174
|
+
* const handleSetFixedPrice = useCallback(() => {
|
|
175
|
+
* // Update the price directly to a hardcoded value
|
|
176
|
+
* editPrice(99.99)
|
|
177
|
+
* }, [editPrice]);
|
|
178
|
+
*
|
|
179
|
+
* return (
|
|
180
|
+
* <div>
|
|
181
|
+
* <p>Current Price: {currentPrice}</p>
|
|
182
|
+
* <button onClick={handleSetFixedPrice}>
|
|
183
|
+
* Set Price to $99.99
|
|
184
|
+
* </button>
|
|
185
|
+
* </div>
|
|
186
|
+
* );
|
|
187
|
+
* }
|
|
188
|
+
*
|
|
189
|
+
* ```
|
|
190
|
+
*
|
|
191
|
+
* @example Usage with Explicit Types (Full Document)
|
|
68
192
|
* ```tsx
|
|
69
193
|
* import React, { useCallback } from 'react';
|
|
70
194
|
* import {useEditDocument, useDocument, type DocumentHandle, type SanityDocument} from '@sanity/sdk-react'
|
|
@@ -136,12 +260,34 @@ export function useEditDocument<TData>(
|
|
|
136
260
|
* }
|
|
137
261
|
*
|
|
138
262
|
* ```
|
|
263
|
+
*
|
|
264
|
+
* @example Edit a document in a release
|
|
265
|
+
* ```tsx
|
|
266
|
+
* import {useEditDocument} from '@sanity/sdk-react'
|
|
267
|
+
*
|
|
268
|
+
* function EditArticleInRelease({documentId}: {documentId: string}) {
|
|
269
|
+
* // Use the document's plain ID — not `versions.<releaseName>.<id>`.
|
|
270
|
+
* // The document must already exist in the release (added via `createDocument` first).
|
|
271
|
+
* const editArticle = useEditDocument({
|
|
272
|
+
* documentId,
|
|
273
|
+
* documentType: 'article',
|
|
274
|
+
* perspective: {releaseName: 'summer-drop'},
|
|
275
|
+
* })
|
|
276
|
+
*
|
|
277
|
+
* return (
|
|
278
|
+
* <button onClick={() => editArticle(prev => ({...prev, title: 'Updated for release'}))}>
|
|
279
|
+
* Edit in Release
|
|
280
|
+
* </button>
|
|
281
|
+
* )
|
|
282
|
+
* }
|
|
283
|
+
* ```
|
|
139
284
|
*/
|
|
140
285
|
export function useEditDocument({
|
|
141
286
|
path,
|
|
142
287
|
...doc
|
|
143
|
-
}:
|
|
288
|
+
}: DocumentOptions<string | undefined>): (updater: Updater<unknown>) => Promise<ActionsResult> {
|
|
144
289
|
const instance = useSanityInstance()
|
|
290
|
+
trackHookUsage(instance, 'useEditDocument')
|
|
145
291
|
const normalizedDoc = useNormalizedResourceOptions(doc)
|
|
146
292
|
|
|
147
293
|
const apply = useApplyDocumentActions()
|