@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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {createSanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {type MutationResult} from '@sanity/sdk/_internal'
|
|
3
|
+
import {act, renderHook} from '@testing-library/react'
|
|
4
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
7
|
+
import {createMutationHook} from './createMutationHook'
|
|
8
|
+
|
|
9
|
+
vi.mock('../context/useSanityInstance', () => ({
|
|
10
|
+
useSanityInstance: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
const instance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
14
|
+
|
|
15
|
+
const settled = <T,>(data: T): MutationResult<T> => ({data, invalidated: Promise.resolve()})
|
|
16
|
+
|
|
17
|
+
describe('createMutationHook', () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks()
|
|
20
|
+
vi.mocked(useSanityInstance).mockReturnValue(instance)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('starts idle', () => {
|
|
24
|
+
const useThing = createMutationHook(vi.fn())
|
|
25
|
+
const {result} = renderHook(() => useThing())
|
|
26
|
+
|
|
27
|
+
expect(result.current.isPending).toBe(false)
|
|
28
|
+
expect(result.current.data).toBeUndefined()
|
|
29
|
+
expect(result.current.error).toBeUndefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('calls the mutation with the instance and resolves with the server data', async () => {
|
|
33
|
+
const mutation = vi.fn((_instance, input: string) =>
|
|
34
|
+
Promise.resolve(settled(`created:${input}`)),
|
|
35
|
+
)
|
|
36
|
+
const useThing = createMutationHook(mutation)
|
|
37
|
+
const {result} = renderHook(() => useThing())
|
|
38
|
+
|
|
39
|
+
let resolved: string | undefined
|
|
40
|
+
await act(async () => {
|
|
41
|
+
resolved = await result.current.mutate('a')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(mutation).toHaveBeenCalledWith(instance, 'a')
|
|
45
|
+
expect(resolved).toBe('created:a')
|
|
46
|
+
expect(result.current.data).toBe('created:a')
|
|
47
|
+
expect(result.current.error).toBeUndefined()
|
|
48
|
+
expect(result.current.isPending).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('reports isPending while the mutation is in flight', async () => {
|
|
52
|
+
let release!: (value: MutationResult<string>) => void
|
|
53
|
+
const mutation = vi.fn(
|
|
54
|
+
() => new Promise<MutationResult<string>>((resolve) => (release = resolve)),
|
|
55
|
+
)
|
|
56
|
+
const useThing = createMutationHook(mutation)
|
|
57
|
+
const {result} = renderHook(() => useThing())
|
|
58
|
+
|
|
59
|
+
act(() => {
|
|
60
|
+
void result.current.mutate('a')
|
|
61
|
+
})
|
|
62
|
+
expect(result.current.isPending).toBe(true)
|
|
63
|
+
|
|
64
|
+
await act(async () => {
|
|
65
|
+
release(settled('done'))
|
|
66
|
+
})
|
|
67
|
+
expect(result.current.isPending).toBe(false)
|
|
68
|
+
expect(result.current.data).toBe('done')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('rejects on failure and captures the error in state', async () => {
|
|
72
|
+
const boom = new Error('nope')
|
|
73
|
+
const mutation = vi.fn(() => Promise.reject(boom))
|
|
74
|
+
const useThing = createMutationHook(mutation)
|
|
75
|
+
const {result} = renderHook(() => useThing())
|
|
76
|
+
|
|
77
|
+
await act(async () => {
|
|
78
|
+
await expect(result.current.mutate('a')).rejects.toBe(boom)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(result.current.error).toBe(boom)
|
|
82
|
+
expect(result.current.data).toBeUndefined()
|
|
83
|
+
expect(result.current.isPending).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('reset() returns to idle', async () => {
|
|
87
|
+
const mutation = vi.fn((_instance, input: string) => Promise.resolve(settled(input)))
|
|
88
|
+
const useThing = createMutationHook(mutation)
|
|
89
|
+
const {result} = renderHook(() => useThing())
|
|
90
|
+
|
|
91
|
+
await act(async () => {
|
|
92
|
+
await result.current.mutate('a')
|
|
93
|
+
})
|
|
94
|
+
expect(result.current.data).toBe('a')
|
|
95
|
+
|
|
96
|
+
act(() => result.current.reset())
|
|
97
|
+
expect(result.current.data).toBeUndefined()
|
|
98
|
+
expect(result.current.error).toBeUndefined()
|
|
99
|
+
expect(result.current.isPending).toBe(false)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('only the latest call writes state, so a stale call cannot clobber it', async () => {
|
|
103
|
+
const releases: Array<(value: MutationResult<string>) => void> = []
|
|
104
|
+
const mutation = vi.fn(
|
|
105
|
+
() => new Promise<MutationResult<string>>((resolve) => releases.push(resolve)),
|
|
106
|
+
)
|
|
107
|
+
const useThing = createMutationHook(mutation)
|
|
108
|
+
const {result} = renderHook(() => useThing())
|
|
109
|
+
|
|
110
|
+
let first!: Promise<string>
|
|
111
|
+
let second!: Promise<string>
|
|
112
|
+
act(() => {
|
|
113
|
+
first = result.current.mutate('first')
|
|
114
|
+
second = result.current.mutate('second')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
await act(async () => {
|
|
118
|
+
releases[1]!(settled('second-result')) // latest resolves first
|
|
119
|
+
releases[0]!(settled('first-result')) // stale resolves after
|
|
120
|
+
await Promise.all([first, second])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(result.current.data).toBe('second-result')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {type SanityInstance} from '@sanity/sdk'
|
|
2
|
+
import {type MutationResult} from '@sanity/sdk/_internal'
|
|
3
|
+
import {useCallback, useRef, useState} from 'react'
|
|
4
|
+
|
|
5
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The value returned by a mutation-backed hook. The write-side counterpart to
|
|
9
|
+
* {@link FetcherHookResult}: local `{mutate, isPending, error, data, reset}`
|
|
10
|
+
* state around a core mutation action.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export interface MutationHookResult<TInput, TResult> {
|
|
15
|
+
/**
|
|
16
|
+
* Runs the mutation. Resolves with the server response; rejects on failure
|
|
17
|
+
* (the failure is also captured in {@link MutationHookResult.error}, so a
|
|
18
|
+
* fire-and-forget caller should read `error` and an `await`ing caller should
|
|
19
|
+
* `try`/`catch`).
|
|
20
|
+
*/
|
|
21
|
+
mutate: (input: TInput) => Promise<TResult>
|
|
22
|
+
/** A mutation is currently in flight. */
|
|
23
|
+
isPending: boolean
|
|
24
|
+
/** The most recent failure; cleared by the next `mutate` or `reset`. */
|
|
25
|
+
error: unknown
|
|
26
|
+
/** The last successful server response, or `undefined` before the first success. */
|
|
27
|
+
data: TResult | undefined
|
|
28
|
+
/** Clears state back to idle and abandons any in-flight result. */
|
|
29
|
+
reset: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type MutationState<TResult> =
|
|
33
|
+
| {status: 'idle'; data: undefined; error: undefined}
|
|
34
|
+
| {status: 'pending'; data: TResult | undefined; error: undefined}
|
|
35
|
+
| {status: 'success'; data: TResult; error: undefined}
|
|
36
|
+
| {status: 'error'; data: TResult | undefined; error: unknown}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds a hook over a core mutation action (the result of `defineMutation`).
|
|
40
|
+
* The write-side counterpart to {@link createFetcherHook}: it binds the action
|
|
41
|
+
* to the current {@link SanityInstance} and tracks `{isPending, error, data}`
|
|
42
|
+
* locally. `mutate` resolves with the server response only — the mutation's
|
|
43
|
+
* `invalidated` promise is intentionally not exposed, since cache
|
|
44
|
+
* reconciliation is handled by the fetcher stores.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function createMutationHook<TInput, TResult>(
|
|
49
|
+
mutation: (instance: SanityInstance, input: TInput) => Promise<MutationResult<TResult>>,
|
|
50
|
+
): () => MutationHookResult<TInput, TResult> {
|
|
51
|
+
const idle: MutationState<TResult> = {status: 'idle', data: undefined, error: undefined}
|
|
52
|
+
|
|
53
|
+
return function useMutationHook(): MutationHookResult<TInput, TResult> {
|
|
54
|
+
const instance = useSanityInstance()
|
|
55
|
+
const [state, setState] = useState<MutationState<TResult>>(idle)
|
|
56
|
+
// Increments per call; only the latest call is allowed to write state, so an
|
|
57
|
+
// earlier in-flight mutation can't clobber a later one (or a `reset`).
|
|
58
|
+
const callId = useRef(0)
|
|
59
|
+
|
|
60
|
+
const mutate = useCallback(
|
|
61
|
+
(input: TInput): Promise<TResult> => {
|
|
62
|
+
const id = ++callId.current
|
|
63
|
+
setState((prev) => ({status: 'pending', data: prev.data, error: undefined}))
|
|
64
|
+
return mutation(instance, input).then(
|
|
65
|
+
({data}) => {
|
|
66
|
+
if (id === callId.current) setState({status: 'success', data, error: undefined})
|
|
67
|
+
return data
|
|
68
|
+
},
|
|
69
|
+
(error: unknown) => {
|
|
70
|
+
if (id === callId.current) {
|
|
71
|
+
setState((prev) => ({status: 'error', data: prev.data, error}))
|
|
72
|
+
}
|
|
73
|
+
throw error
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
},
|
|
77
|
+
[instance],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const reset = useCallback(() => {
|
|
81
|
+
callId.current++ // abandon any in-flight response so it can't revive stale state
|
|
82
|
+
setState(idle)
|
|
83
|
+
}, [])
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
mutate,
|
|
87
|
+
isPending: state.status === 'pending',
|
|
88
|
+
error: state.error,
|
|
89
|
+
data: state.data,
|
|
90
|
+
reset,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {createSanityInstance, type
|
|
1
|
+
import {createSanityInstance, type SanityInstance} from '@sanity/sdk'
|
|
2
2
|
import {renderHook} from '@testing-library/react'
|
|
3
3
|
import {throwError} from 'rxjs'
|
|
4
4
|
import {describe, expect, it, vi} from 'vitest'
|
|
@@ -17,7 +17,7 @@ describe('createStateSourceHook', () => {
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
it('should create a hook that provides access to state source', () => {
|
|
20
|
-
const mockInstance = createSanityInstance()
|
|
20
|
+
const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
21
21
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
22
22
|
|
|
23
23
|
const mockState = {count: 0}
|
|
@@ -39,7 +39,7 @@ describe('createStateSourceHook', () => {
|
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
it('should recreate state source when params change', () => {
|
|
42
|
-
const mockInstance = createSanityInstance()
|
|
42
|
+
const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
43
43
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
44
44
|
|
|
45
45
|
const subscribe = vi.fn()
|
|
@@ -65,14 +65,14 @@ describe('createStateSourceHook', () => {
|
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
it('should recreate state source when instance changes', () => {
|
|
68
|
-
const mockInstance1 = createSanityInstance({
|
|
69
|
-
const mockInstance2 = createSanityInstance({
|
|
68
|
+
const mockInstance1 = createSanityInstance({projectId: 'p1', dataset: 'd'})
|
|
69
|
+
const mockInstance2 = createSanityInstance({projectId: 'p2', dataset: 'd'})
|
|
70
70
|
|
|
71
71
|
vi.mocked(useSanityInstance).mockReturnValueOnce(mockInstance1)
|
|
72
72
|
|
|
73
73
|
const stateSourceFactory = vi.fn((instance: SanityInstance) => ({
|
|
74
74
|
subscribe: vi.fn(),
|
|
75
|
-
getCurrent: () =>
|
|
75
|
+
getCurrent: () => instance.config.projectId,
|
|
76
76
|
observable: throwError(() => new Error('unexpected usage of observable')),
|
|
77
77
|
}))
|
|
78
78
|
|
|
@@ -89,7 +89,7 @@ describe('createStateSourceHook', () => {
|
|
|
89
89
|
})
|
|
90
90
|
|
|
91
91
|
it('should handle subscription functionality', () => {
|
|
92
|
-
const mockInstance = createSanityInstance()
|
|
92
|
+
const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
93
93
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
94
94
|
|
|
95
95
|
const mockSubscribe = vi.fn()
|
|
@@ -110,7 +110,7 @@ describe('createStateSourceHook', () => {
|
|
|
110
110
|
})
|
|
111
111
|
|
|
112
112
|
it('should handle multiple parameters', () => {
|
|
113
|
-
const mockInstance = createSanityInstance()
|
|
113
|
+
const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
114
114
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
115
115
|
|
|
116
116
|
const stateSourceFactory = vi.fn(
|
|
@@ -129,7 +129,7 @@ describe('createStateSourceHook', () => {
|
|
|
129
129
|
})
|
|
130
130
|
|
|
131
131
|
it('should throw suspender promise when shouldSuspend is true', () => {
|
|
132
|
-
const mockInstance = createSanityInstance()
|
|
132
|
+
const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
133
133
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
134
134
|
|
|
135
135
|
const mockGetState = vi.fn().mockReturnValue({
|
|
@@ -163,7 +163,7 @@ describe('createStateSourceHook', () => {
|
|
|
163
163
|
})
|
|
164
164
|
|
|
165
165
|
it('should not suspend when shouldSuspend returns false', () => {
|
|
166
|
-
const mockInstance = createSanityInstance()
|
|
166
|
+
const mockInstance = createSanityInstance({projectId: 'p', dataset: 'd'})
|
|
167
167
|
vi.mocked(useSanityInstance).mockReturnValue(mockInstance)
|
|
168
168
|
|
|
169
169
|
const mockState = {value: 'test'}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
1
|
+
import {type SanityConfig, type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
2
2
|
import {useSyncExternalStore} from 'react'
|
|
3
3
|
|
|
4
4
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -12,6 +12,7 @@ interface CreateStateSourceHookOptions<TParams extends unknown[], TState> {
|
|
|
12
12
|
getState: StateSourceFactory<TParams, TState>
|
|
13
13
|
shouldSuspend?: (instance: SanityInstance, ...params: TParams) => boolean
|
|
14
14
|
suspender?: (instance: SanityInstance, ...params: TParams) => Promise<unknown>
|
|
15
|
+
getConfig?: (...params: TParams) => SanityConfig | undefined
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function createStateSourceHook<TParams extends unknown[], TState>(
|
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
|
+
}
|