@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
package/src/hooks/{document/useApplyDocumentActions.test.ts → helpers/useApplyActions.test.tsx}
RENAMED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {applyDocumentActions,
|
|
2
|
-
import
|
|
1
|
+
import {applyDocumentActions, createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {renderHook as reactRenderHook} from '@testing-library/react'
|
|
3
3
|
import {describe, it} from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {renderHook} from '../../../test/test-utils'
|
|
6
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
6
7
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
-
import {
|
|
8
|
+
import {useApplyActions} from './useApplyActions'
|
|
8
9
|
|
|
9
10
|
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
10
11
|
const original = await importOriginal<typeof import('@sanity/sdk')>()
|
|
@@ -13,21 +14,20 @@ vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
|
13
14
|
|
|
14
15
|
vi.mock('../context/useSanityInstance')
|
|
15
16
|
|
|
16
|
-
const instance = {
|
|
17
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
17
18
|
|
|
18
|
-
describe('
|
|
19
|
+
describe('useApplyActions', () => {
|
|
19
20
|
beforeEach(() => {
|
|
20
21
|
vi.resetAllMocks()
|
|
21
22
|
vi.mocked(useSanityInstance).mockReturnValueOnce(instance)
|
|
22
23
|
})
|
|
23
24
|
|
|
24
|
-
it('uses the
|
|
25
|
-
const {result} = renderHook(() =>
|
|
25
|
+
it('uses the effective context resource', async () => {
|
|
26
|
+
const {result} = renderHook(() => useApplyActions())
|
|
26
27
|
result.current({
|
|
27
28
|
type: 'document.edit',
|
|
28
29
|
documentType: 'post',
|
|
29
30
|
documentId: 'abc',
|
|
30
|
-
resource: {projectId: 'p123', dataset: 'd123'},
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
@@ -36,21 +36,28 @@ describe('useApplyDocumentActions', () => {
|
|
|
36
36
|
type: 'document.edit',
|
|
37
37
|
documentType: 'post',
|
|
38
38
|
documentId: 'abc',
|
|
39
|
-
resource
|
|
39
|
+
// resource named in test-utils
|
|
40
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
40
41
|
},
|
|
41
42
|
],
|
|
42
|
-
resource: {projectId: '
|
|
43
|
+
resource: {projectId: 'test', dataset: 'test'},
|
|
43
44
|
})
|
|
44
45
|
})
|
|
45
46
|
|
|
46
|
-
it('
|
|
47
|
-
const {result} =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
documentId: 'abc',
|
|
52
|
-
resource: {projectId: 'p123', dataset: 'd123'},
|
|
47
|
+
it('uses the SanityInstance when resource is not provided', async () => {
|
|
48
|
+
const {result} = reactRenderHook(() => useApplyActions(), {
|
|
49
|
+
wrapper: ({children}) => (
|
|
50
|
+
<SanityInstanceContext.Provider value={instance}>{children}</SanityInstanceContext.Provider>
|
|
51
|
+
),
|
|
53
52
|
})
|
|
53
|
+
result.current(
|
|
54
|
+
{
|
|
55
|
+
type: 'document.edit',
|
|
56
|
+
documentType: 'post',
|
|
57
|
+
documentId: 'abc',
|
|
58
|
+
},
|
|
59
|
+
{},
|
|
60
|
+
)
|
|
54
61
|
|
|
55
62
|
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
56
63
|
actions: [
|
|
@@ -58,20 +65,21 @@ describe('useApplyDocumentActions', () => {
|
|
|
58
65
|
type: 'document.edit',
|
|
59
66
|
documentType: 'post',
|
|
60
67
|
documentId: 'abc',
|
|
61
|
-
resource: {projectId: '
|
|
68
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
62
69
|
},
|
|
63
70
|
],
|
|
64
|
-
resource: {projectId: '
|
|
71
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
65
72
|
})
|
|
66
73
|
})
|
|
67
74
|
|
|
68
|
-
it('
|
|
69
|
-
const {result} = renderHook(() =>
|
|
75
|
+
it('resolves resource from projectId and dataset on the action', async () => {
|
|
76
|
+
const {result} = renderHook(() => useApplyActions())
|
|
70
77
|
result.current({
|
|
71
78
|
type: 'document.edit',
|
|
72
79
|
documentType: 'post',
|
|
73
80
|
documentId: 'abc',
|
|
74
|
-
|
|
81
|
+
projectId: 'p',
|
|
82
|
+
dataset: 'd123',
|
|
75
83
|
})
|
|
76
84
|
|
|
77
85
|
expect(applyDocumentActions).toHaveBeenCalledExactlyOnceWith(instance, {
|
|
@@ -80,55 +88,59 @@ describe('useApplyDocumentActions', () => {
|
|
|
80
88
|
type: 'document.edit',
|
|
81
89
|
documentType: 'post',
|
|
82
90
|
documentId: 'abc',
|
|
83
|
-
resource: {projectId: '
|
|
91
|
+
resource: {projectId: 'p', dataset: 'd123'},
|
|
84
92
|
},
|
|
85
93
|
],
|
|
86
|
-
resource: {projectId: '
|
|
94
|
+
resource: {projectId: 'p', dataset: 'd123'},
|
|
87
95
|
})
|
|
88
96
|
})
|
|
89
97
|
|
|
90
|
-
it('throws when actions have mismatched
|
|
91
|
-
const {result} = renderHook(() =>
|
|
98
|
+
it('throws when actions have mismatched project IDs', async () => {
|
|
99
|
+
const {result} = renderHook(() => useApplyActions())
|
|
92
100
|
expect(() => {
|
|
93
101
|
result.current([
|
|
94
102
|
{
|
|
95
103
|
type: 'document.edit',
|
|
96
104
|
documentType: 'post',
|
|
97
105
|
documentId: 'abc',
|
|
98
|
-
|
|
106
|
+
projectId: 'p123',
|
|
107
|
+
dataset: 'd',
|
|
99
108
|
},
|
|
100
109
|
{
|
|
101
110
|
type: 'document.edit',
|
|
102
111
|
documentType: 'post',
|
|
103
112
|
documentId: 'def',
|
|
104
|
-
|
|
113
|
+
projectId: 'p456',
|
|
114
|
+
dataset: 'd',
|
|
105
115
|
},
|
|
106
116
|
])
|
|
107
117
|
}).toThrow(/Mismatched resources found in actions/)
|
|
108
118
|
})
|
|
109
119
|
|
|
110
|
-
it('throws when actions have mismatched
|
|
111
|
-
const {result} = renderHook(() =>
|
|
120
|
+
it('throws when actions have mismatched datasets', async () => {
|
|
121
|
+
const {result} = renderHook(() => useApplyActions())
|
|
112
122
|
expect(() => {
|
|
113
123
|
result.current([
|
|
114
124
|
{
|
|
115
125
|
type: 'document.edit',
|
|
116
126
|
documentType: 'post',
|
|
117
127
|
documentId: 'abc',
|
|
118
|
-
|
|
128
|
+
projectId: 'p',
|
|
129
|
+
dataset: 'd1',
|
|
119
130
|
},
|
|
120
131
|
{
|
|
121
132
|
type: 'document.edit',
|
|
122
133
|
documentType: 'post',
|
|
123
134
|
documentId: 'def',
|
|
124
|
-
|
|
135
|
+
projectId: 'p',
|
|
136
|
+
dataset: 'd2',
|
|
125
137
|
},
|
|
126
138
|
])
|
|
127
139
|
}).toThrow(/Mismatched resources found in actions/)
|
|
128
140
|
})
|
|
129
141
|
|
|
130
|
-
it('throws when actions have mismatched
|
|
131
|
-
const {result} = renderHook(() =>
|
|
142
|
+
it('throws when actions have mismatched resources', async () => {
|
|
143
|
+
const {result} = renderHook(() => useApplyActions())
|
|
132
144
|
expect(() => {
|
|
133
145
|
result.current([
|
|
134
146
|
{
|
|
@@ -147,34 +159,61 @@ describe('useApplyDocumentActions', () => {
|
|
|
147
159
|
}).toThrow(/Mismatched resources found in actions/)
|
|
148
160
|
})
|
|
149
161
|
|
|
150
|
-
it('throws when
|
|
151
|
-
|
|
152
|
-
const {result} = renderHook(() => useApplyDocumentActions(), {
|
|
153
|
-
wrapper: (props) => React.createElement(React.Fragment, null, props.children),
|
|
154
|
-
})
|
|
155
|
-
|
|
162
|
+
it('throws when mixing projectId/dataset and resource with a mismatch (projectId first)', async () => {
|
|
163
|
+
const {result} = renderHook(() => useApplyActions())
|
|
156
164
|
expect(() => {
|
|
157
|
-
result.current(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
result.current([
|
|
166
|
+
{
|
|
167
|
+
type: 'document.edit',
|
|
168
|
+
documentType: 'post',
|
|
169
|
+
documentId: 'abc',
|
|
170
|
+
projectId: 'p1',
|
|
171
|
+
dataset: 'd',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'document.edit',
|
|
175
|
+
documentType: 'post',
|
|
176
|
+
documentId: 'def',
|
|
177
|
+
resource: {projectId: 'p2', dataset: 'd'},
|
|
178
|
+
},
|
|
179
|
+
])
|
|
180
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
164
181
|
})
|
|
165
182
|
|
|
166
|
-
it('throws when
|
|
167
|
-
const {result} = renderHook(() =>
|
|
168
|
-
|
|
183
|
+
it('throws when mixing resource and projectId/dataset with a mismatch (resource first)', async () => {
|
|
184
|
+
const {result} = renderHook(() => useApplyActions())
|
|
169
185
|
expect(() => {
|
|
170
|
-
result.current(
|
|
186
|
+
result.current([
|
|
171
187
|
{
|
|
172
188
|
type: 'document.edit',
|
|
173
189
|
documentType: 'post',
|
|
174
190
|
documentId: 'abc',
|
|
175
|
-
resource: {projectId: 'p1', dataset: '
|
|
191
|
+
resource: {projectId: 'p1', dataset: 'd'},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: 'document.edit',
|
|
195
|
+
documentType: 'post',
|
|
196
|
+
documentId: 'def',
|
|
197
|
+
projectId: 'p2',
|
|
198
|
+
dataset: 'd',
|
|
176
199
|
},
|
|
177
|
-
|
|
200
|
+
])
|
|
201
|
+
}).toThrow(/Mismatched resources found in actions/)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('throws when a top-level options resource conflicts with an action resource', async () => {
|
|
205
|
+
const {result} = renderHook(() => useApplyActions())
|
|
206
|
+
expect(() => {
|
|
207
|
+
result.current(
|
|
208
|
+
[
|
|
209
|
+
{
|
|
210
|
+
type: 'document.edit',
|
|
211
|
+
documentType: 'post',
|
|
212
|
+
documentId: 'abc',
|
|
213
|
+
resource: {projectId: 'p', dataset: 'd1'},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
{resource: {projectId: 'p', dataset: 'd2'}},
|
|
178
217
|
)
|
|
179
218
|
}).toThrow(/Mismatched resources found in actions/)
|
|
180
219
|
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {type Action, type ActionsResult, applyDocumentActions} from '@sanity/sdk'
|
|
2
|
+
import {isDeepEqual} from '@sanity/sdk/_internal'
|
|
3
|
+
import {useContext} from 'react'
|
|
4
|
+
|
|
5
|
+
import {type ResourceHandle} from '../../config/handles'
|
|
6
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
7
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
8
|
+
import {normalizeResourceOptions, useEffectiveContextResource} from './useNormalizedResourceOptions'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*
|
|
13
|
+
* Shared implementation behind `useApplyDocumentActions` and
|
|
14
|
+
* `useApplyReleaseActions`. Resolves the effective resource from the action
|
|
15
|
+
* handles, top-level `options`, or context (in that order), validates that
|
|
16
|
+
* everything agrees on a single resource, and forwards to
|
|
17
|
+
* `applyDocumentActions`.
|
|
18
|
+
*
|
|
19
|
+
* The two public hooks differ only in their input/output types — at runtime
|
|
20
|
+
* they're identical because core's `applyDocumentActions` accepts the full
|
|
21
|
+
* `Action` union.
|
|
22
|
+
*/
|
|
23
|
+
export function useApplyActions(): (
|
|
24
|
+
actionOrActions: Action | Action[],
|
|
25
|
+
options?: ResourceHandle,
|
|
26
|
+
) => Promise<ActionsResult> {
|
|
27
|
+
const instance = useSanityInstance()
|
|
28
|
+
const resources = useContext(ResourcesContext)
|
|
29
|
+
const effectiveContextResource = useEffectiveContextResource()
|
|
30
|
+
|
|
31
|
+
return (actionOrActions, options) => {
|
|
32
|
+
const actions = Array.isArray(actionOrActions) ? actionOrActions : [actionOrActions]
|
|
33
|
+
const optionsResource = options
|
|
34
|
+
? normalizeResourceOptions(options, resources, effectiveContextResource).resource
|
|
35
|
+
: undefined
|
|
36
|
+
|
|
37
|
+
const normalizedActions = actions.map((action) =>
|
|
38
|
+
normalizeResourceOptions(action, resources, effectiveContextResource),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
let resource
|
|
42
|
+
for (const action of normalizedActions) {
|
|
43
|
+
const actionResource = action['resource']
|
|
44
|
+
if (!resource && actionResource) resource = actionResource
|
|
45
|
+
if (!isDeepEqual(actionResource, resource)) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Mismatched resources found in actions. All actions must belong to the same resource. Found "${JSON.stringify(actionResource)}" but expected "${JSON.stringify(resource)}".`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (optionsResource && resource && !isDeepEqual(optionsResource, resource)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Mismatched resources found in actions. Found top-level resource "${JSON.stringify(optionsResource)}" but expected resource from action handles "${JSON.stringify(resource)}".`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const effectiveResource = resource ?? optionsResource ?? effectiveContextResource
|
|
59
|
+
if (!effectiveResource) {
|
|
60
|
+
throw new Error('No resource found. Provide a resource via the action handle or context.')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return applyDocumentActions(instance, {
|
|
64
|
+
actions: normalizedActions as Action[],
|
|
65
|
+
resource: effectiveResource,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {createSanityInstance, type DocumentHandle} from '@sanity/sdk'
|
|
2
|
+
import {type ReactNode} from 'react'
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {renderHook, resources} from '../../../test/test-utils'
|
|
6
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
7
|
+
import {ResourcesContext} from '../../context/ResourcesContext'
|
|
8
|
+
import {SanityInstanceContext} from '../../context/SanityInstanceContext'
|
|
9
|
+
import {useNormalizedResourceOptions} from './useNormalizedResourceOptions'
|
|
10
|
+
|
|
11
|
+
// Wrapper that sets ResourceContext via the `resource` prop (tier 3).
|
|
12
|
+
// Includes ResourcesContext so resourceName resolution also works in these tests.
|
|
13
|
+
function ResourceContextWrapper({
|
|
14
|
+
children,
|
|
15
|
+
resource,
|
|
16
|
+
}: {
|
|
17
|
+
children: ReactNode
|
|
18
|
+
resource: {projectId: string; dataset: string}
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<ResourceProvider resource={resource} fallback={null}>
|
|
22
|
+
<ResourcesContext.Provider value={resources}>{children}</ResourcesContext.Provider>
|
|
23
|
+
</ResourceProvider>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Wrapper that provides an instance with no projectId/dataset and no ResourceContext (tier 5).
|
|
28
|
+
const bareInstance = createSanityInstance({})
|
|
29
|
+
function NoResourceWrapper({children}: {children: ReactNode}) {
|
|
30
|
+
return (
|
|
31
|
+
<SanityInstanceContext.Provider value={bareInstance}>{children}</SanityInstanceContext.Provider>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('useNormalizedResourceOptions', () => {
|
|
36
|
+
describe('tier 1 — explicit options', () => {
|
|
37
|
+
it('uses an explicit dataset resource object', () => {
|
|
38
|
+
const {result} = renderHook(() =>
|
|
39
|
+
useNormalizedResourceOptions({resource: {projectId: 'explicit', dataset: 'explicit-ds'}}),
|
|
40
|
+
)
|
|
41
|
+
expect(result.current.resource).toEqual({projectId: 'explicit', dataset: 'explicit-ds'})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('uses an explicit media-library resource object', () => {
|
|
45
|
+
const {result} = renderHook(() =>
|
|
46
|
+
useNormalizedResourceOptions({resource: {mediaLibraryId: 'ml-123'}}),
|
|
47
|
+
)
|
|
48
|
+
expect(result.current.resource).toEqual({mediaLibraryId: 'ml-123'})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('uses an explicit canvas resource object', () => {
|
|
52
|
+
const {result} = renderHook(() =>
|
|
53
|
+
useNormalizedResourceOptions({resource: {canvasId: 'canvas-123'}}),
|
|
54
|
+
)
|
|
55
|
+
expect(result.current.resource).toEqual({canvasId: 'canvas-123'})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('resolves resourceName to a named dataset resource', () => {
|
|
59
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({resourceName: 'dataset'}))
|
|
60
|
+
expect(result.current.resource).toEqual({
|
|
61
|
+
projectId: 'resource-project-id',
|
|
62
|
+
dataset: 'resource-dataset',
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('resolves resourceName to a named media-library resource', () => {
|
|
67
|
+
const {result} = renderHook(() =>
|
|
68
|
+
useNormalizedResourceOptions({resourceName: 'media-library'}),
|
|
69
|
+
)
|
|
70
|
+
expect(result.current.resource).toEqual({mediaLibraryId: 'media-library-id'})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('resolves resourceName to a named canvas resource', () => {
|
|
74
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({resourceName: 'canvas'}))
|
|
75
|
+
expect(result.current.resource).toEqual({canvasId: 'canvas-id'})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('throws when resourceName is not registered', () => {
|
|
79
|
+
expect(() =>
|
|
80
|
+
renderHook(() => useNormalizedResourceOptions({resourceName: 'unknown'})),
|
|
81
|
+
).toThrow(/no resource named/i)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('throws when both resource and resourceName are provided', () => {
|
|
85
|
+
expect(() =>
|
|
86
|
+
renderHook(() =>
|
|
87
|
+
useNormalizedResourceOptions({
|
|
88
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
89
|
+
resourceName: 'dataset',
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
).toThrow()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('tier 2 — bare projectId/dataset in options', () => {
|
|
97
|
+
it('synthesizes a resource from projectId + dataset', () => {
|
|
98
|
+
const {result} = renderHook(() =>
|
|
99
|
+
useNormalizedResourceOptions({projectId: 'opt', dataset: 'opt-ds'}),
|
|
100
|
+
)
|
|
101
|
+
expect(result.current.resource).toEqual({projectId: 'opt', dataset: 'opt-ds'})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('falls through to context when only projectId is provided (no dataset)', () => {
|
|
105
|
+
// Only projectId is not enough to synthesize — should fall back to context resource
|
|
106
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({projectId: 'opt'}))
|
|
107
|
+
// Default test-utils: ResourceProvider projectId="test" dataset="test" → tier-3 via config synthesis
|
|
108
|
+
expect(result.current.resource).toEqual({projectId: 'test', dataset: 'test'})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('tier 3 — ResourceContext', () => {
|
|
113
|
+
it('uses ResourceContext set via ResourceProvider `resource` prop', () => {
|
|
114
|
+
const contextResource = {projectId: 'ctx-project', dataset: 'ctx-dataset'}
|
|
115
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
116
|
+
wrapper: ({children}) => (
|
|
117
|
+
<ResourceContextWrapper resource={contextResource}>{children}</ResourceContextWrapper>
|
|
118
|
+
),
|
|
119
|
+
})
|
|
120
|
+
expect(result.current.resource).toEqual(contextResource)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('uses ResourceContext synthesized from ResourceProvider projectId/dataset', () => {
|
|
124
|
+
// ResourceProvider with projectId/dataset (no explicit resource prop) synthesizes ResourceContext
|
|
125
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}))
|
|
126
|
+
// Default test-utils: ResourceProvider projectId="test" dataset="test"
|
|
127
|
+
expect(result.current.resource).toEqual({projectId: 'test', dataset: 'test'})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('explicit resource in options takes precedence over ResourceContext', () => {
|
|
131
|
+
const {result} = renderHook(
|
|
132
|
+
() =>
|
|
133
|
+
useNormalizedResourceOptions({resource: {projectId: 'explicit', dataset: 'explicit-ds'}}),
|
|
134
|
+
{
|
|
135
|
+
wrapper: ({children}) => (
|
|
136
|
+
<ResourceContextWrapper resource={{projectId: 'ctx-project', dataset: 'ctx-dataset'}}>
|
|
137
|
+
{children}
|
|
138
|
+
</ResourceContextWrapper>
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
expect(result.current.resource).toEqual({projectId: 'explicit', dataset: 'explicit-ds'})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('tier 4 — SanityInstance config fallback', () => {
|
|
147
|
+
it('falls back to instance projectId/dataset when ResourceContext is not set', () => {
|
|
148
|
+
// Bare SanityInstanceContext with config — no ResourceProvider, so no ResourceContext
|
|
149
|
+
const instanceWithConfig = createSanityInstance({projectId: 'inst', dataset: 'inst-ds'})
|
|
150
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
151
|
+
wrapper: ({children}) => (
|
|
152
|
+
<SanityInstanceContext.Provider value={instanceWithConfig}>
|
|
153
|
+
{children}
|
|
154
|
+
</SanityInstanceContext.Provider>
|
|
155
|
+
),
|
|
156
|
+
})
|
|
157
|
+
expect(result.current.resource).toEqual({projectId: 'inst', dataset: 'inst-ds'})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('tier 5 — no resource available', () => {
|
|
162
|
+
it('returns no resource when neither options, context, nor instance config provide one', () => {
|
|
163
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
164
|
+
wrapper: NoResourceWrapper,
|
|
165
|
+
})
|
|
166
|
+
expect(result.current).not.toHaveProperty('resource')
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('perspective resolution', () => {
|
|
171
|
+
it('uses explicit perspective from options', () => {
|
|
172
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({perspective: 'published'}))
|
|
173
|
+
expect(result.current.perspective).toBe('published')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('falls back to PerspectiveContext when no perspective in options', () => {
|
|
177
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
178
|
+
wrapper: ({children}) => (
|
|
179
|
+
<ResourceProvider perspective="previewDrafts" fallback={null}>
|
|
180
|
+
{children}
|
|
181
|
+
</ResourceProvider>
|
|
182
|
+
),
|
|
183
|
+
})
|
|
184
|
+
expect(result.current.perspective).toBe('previewDrafts')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('explicit perspective overrides PerspectiveContext', () => {
|
|
188
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({perspective: 'published'}), {
|
|
189
|
+
wrapper: ({children}) => (
|
|
190
|
+
<ResourceProvider perspective="previewDrafts" fallback={null}>
|
|
191
|
+
{children}
|
|
192
|
+
</ResourceProvider>
|
|
193
|
+
),
|
|
194
|
+
})
|
|
195
|
+
expect(result.current.perspective).toBe('published')
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('omits perspective from result when not set', () => {
|
|
199
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({}), {
|
|
200
|
+
wrapper: NoResourceWrapper,
|
|
201
|
+
})
|
|
202
|
+
expect(result.current).not.toHaveProperty('perspective')
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('field stripping', () => {
|
|
207
|
+
it('strips resourceName from the result', () => {
|
|
208
|
+
const {result} = renderHook(() => useNormalizedResourceOptions({resourceName: 'dataset'}))
|
|
209
|
+
expect(result.current).not.toHaveProperty('resourceName')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('strips projectId and dataset from the result when synthesized into resource', () => {
|
|
213
|
+
const {result} = renderHook(() =>
|
|
214
|
+
useNormalizedResourceOptions({projectId: 'p', dataset: 'd'}),
|
|
215
|
+
)
|
|
216
|
+
expect(result.current).not.toHaveProperty('projectId')
|
|
217
|
+
expect(result.current).not.toHaveProperty('dataset')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('preserves unrelated fields', () => {
|
|
221
|
+
const opts: DocumentHandle = {documentId: 'doc-1', documentType: 'article'}
|
|
222
|
+
const {result} = renderHook(() => useNormalizedResourceOptions(opts))
|
|
223
|
+
expect(result.current).toMatchObject({documentId: 'doc-1', documentType: 'article'})
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
})
|