@sanity/sdk-react 3.0.0-rc.1 → 3.0.0
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/dist/_exports/dashboard.d.ts +209 -0
- package/dist/_exports/dashboard.d.ts.map +1 -0
- package/dist/_exports/dashboard.js +278 -0
- package/dist/_exports/dashboard.js.map +1 -0
- package/dist/index.d.ts +2186 -2774
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3953 -1951
- package/dist/index.js.map +1 -1
- package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js +177 -0
- package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js.map +1 -0
- package/package.json +19 -19
- package/src/_exports/dashboard.ts +11 -0
- package/src/_exports/sdk-react.ts +11 -18
- package/src/components/SDKProvider.test.tsx +5 -5
- package/src/components/SanityApp.tsx +2 -3
- package/src/components/auth/AuthBoundary.test.tsx +1 -19
- package/src/components/auth/AuthBoundary.tsx +7 -6
- package/src/components/auth/LoginError.tsx +2 -3
- package/src/context/ComlinkTokenRefresh.tsx +3 -5
- package/src/context/{WorkbenchTokenRefresh.test.tsx → DashboardTokenRefresh.test.tsx} +26 -26
- package/src/context/{WorkbenchTokenRefresh.tsx → DashboardTokenRefresh.tsx} +14 -14
- package/src/context/OrganizationResourcesProvider.test.tsx +9 -9
- package/src/context/OrganizationResourcesProvider.tsx +2 -2
- package/src/context/ResourceProvider.test.tsx +21 -0
- package/src/context/ResourceProvider.tsx +4 -1
- package/src/context/{workbenchToken.ts → dashboardToken.ts} +13 -13
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +1 -1
- 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 +3 -4
- 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/useSanityInstance.test.tsx +2 -54
- package/src/hooks/context/useSanityInstance.ts +2 -24
- package/src/hooks/{agent → dashboard}/useAgentResourceContext.ts +2 -2
- package/src/hooks/dashboard/useFavorite.test.tsx +101 -0
- package/src/hooks/dashboard/useFavorite.ts +34 -0
- package/src/hooks/dashboard/useFavoriteContext.ts +61 -0
- package/src/hooks/dashboard/{useDashboardNavigate.test.ts → useNavigate.test.ts} +3 -3
- package/src/hooks/dashboard/{useDashboardNavigate.ts → useNavigate.ts} +5 -5
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -1
- package/src/hooks/{auth/useDashboardOrganizationId.test.tsx → dashboard/useOrganizationId.test.tsx} +4 -4
- package/src/hooks/{auth/useDashboardOrganizationId.tsx → dashboard/useOrganizationId.tsx} +2 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -1
- package/src/hooks/dashboard/useUpdateFavorite.test.tsx +146 -0
- package/src/hooks/dashboard/useUpdateFavorite.ts +74 -0
- package/src/hooks/dashboard/useWindowTitle.ts +1 -1
- package/src/hooks/datasets/useDatasets.test.tsx +29 -22
- package/src/hooks/datasets/useDatasets.ts +31 -53
- package/src/hooks/documents/useDocuments.ts +2 -7
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +0 -27
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +14 -25
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +2 -7
- package/src/hooks/presence/usePresence.ts +14 -1
- 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.tsx +2 -6
- package/src/hooks/projects/useProjects.ts +0 -8
- package/src/hooks/query/useQuery.ts +3 -8
- package/src/hooks/releases/useActiveReleases.ts +2 -2
- package/src/hooks/releases/useAllReleases.ts +2 -2
- package/src/hooks/users/useUser.ts +2 -8
- package/src/hooks/users/useUsers.ts +1 -2
- package/src/hooks/applications/useCreateUserApplication.test-d.ts +0 -14
- package/src/hooks/applications/useCreateUserApplication.ts +0 -11
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +0 -14
- package/src/hooks/applications/useDeleteUserApplication.ts +0 -11
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +0 -14
- package/src/hooks/applications/useUpdateUserApplication.ts +0 -11
- package/src/hooks/applications/useUserApplication.test-d.ts +0 -11
- package/src/hooks/applications/useUserApplication.ts +0 -14
- package/src/hooks/applications/useUserApplications.test-d.ts +0 -11
- package/src/hooks/applications/useUserApplications.ts +0 -14
- package/src/hooks/dashboard/useManageFavorite.test.tsx +0 -379
- package/src/hooks/dashboard/useManageFavorite.ts +0 -173
- /package/src/hooks/{agent → dashboard}/useAgentResourceContext.test.tsx +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {reportPresence} from '@sanity/sdk'
|
|
2
|
+
import {act, renderHook} from '@testing-library/react'
|
|
3
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {ResourceProvider} from '../../context/ResourceProvider'
|
|
6
|
+
import {useReportPresence} from './useReportPresence'
|
|
7
|
+
|
|
8
|
+
vi.mock('@sanity/sdk', async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal<typeof import('@sanity/sdk')>()
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
reportPresence: vi.fn(),
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const reported = () => vi.mocked(reportPresence).mock.calls.map(([, params]) => params.locations)
|
|
17
|
+
|
|
18
|
+
const wrapper = ({children}: {children: React.ReactNode}) => (
|
|
19
|
+
<ResourceProvider projectId="p" dataset="d" fallback={null}>
|
|
20
|
+
{children}
|
|
21
|
+
</ResourceProvider>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
describe('useReportPresence', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks()
|
|
27
|
+
vi.useFakeTimers()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.useRealTimers()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('reports the document with no path or selection keys when neither was given', () => {
|
|
35
|
+
renderHook(() => useReportPresence({documentId: 'doc-1', documentType: 'movie'}), {wrapper})
|
|
36
|
+
|
|
37
|
+
// Keys absent rather than set to `undefined`. The id stays unresolved here:
|
|
38
|
+
// core turns a perspective into a specific document id, so the read and write
|
|
39
|
+
// sides cannot drift apart.
|
|
40
|
+
expect(reported()).toEqual([[{documentId: 'doc-1'}]])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('forwards an explicit perspective for core to resolve', () => {
|
|
44
|
+
renderHook(
|
|
45
|
+
() =>
|
|
46
|
+
useReportPresence({
|
|
47
|
+
documentId: 'doc-1',
|
|
48
|
+
documentType: 'movie',
|
|
49
|
+
perspective: {releaseName: 'autumn'},
|
|
50
|
+
}),
|
|
51
|
+
{wrapper},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
expect(reported()[0][0]).toMatchObject({
|
|
55
|
+
documentId: 'doc-1',
|
|
56
|
+
perspective: {releaseName: 'autumn'},
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('forwards liveEdit, which core resolves to the published document', () => {
|
|
61
|
+
renderHook(
|
|
62
|
+
() => useReportPresence({documentId: 'doc-1', documentType: 'movie', liveEdit: true}),
|
|
63
|
+
{wrapper},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
expect(reported()[0][0]).toMatchObject({documentId: 'doc-1', liveEdit: true})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('picks up an ambient perspective from the provider', () => {
|
|
70
|
+
// The whole point of resolving after normalization: a perspective set once on
|
|
71
|
+
// `ResourceProvider` has to reach presence without every call site passing it.
|
|
72
|
+
const ambient = ({children}: {children: React.ReactNode}) => (
|
|
73
|
+
<ResourceProvider projectId="p" dataset="d" perspective="published" fallback={null}>
|
|
74
|
+
{children}
|
|
75
|
+
</ResourceProvider>
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
renderHook(() => useReportPresence({documentId: 'doc-1', documentType: 'movie'}), {
|
|
79
|
+
wrapper: ambient,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(reported()[0][0]).toMatchObject({documentId: 'doc-1', perspective: 'published'})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('announces a field path', () => {
|
|
86
|
+
renderHook(
|
|
87
|
+
() => useReportPresence({documentId: 'doc-1', documentType: 'movie', path: ['title']}),
|
|
88
|
+
{wrapper},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
expect(reported()[0][0].path).toEqual(['title'])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('carries keyed segments and a selection', () => {
|
|
95
|
+
const path = ['body', {_key: 'b1'}, 'children', {_key: 's1'}, 'text']
|
|
96
|
+
const selection = {
|
|
97
|
+
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 1},
|
|
98
|
+
focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 4},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
renderHook(
|
|
102
|
+
() => useReportPresence({documentId: 'doc-1', documentType: 'movie', path, selection}),
|
|
103
|
+
{wrapper},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
expect(reported()[0][0].path).toEqual(path)
|
|
107
|
+
expect(reported()[0][0].selection).toEqual(selection)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('does not re-announce when a caller passes fresh literals each render', () => {
|
|
111
|
+
const {rerender} = renderHook(
|
|
112
|
+
() =>
|
|
113
|
+
useReportPresence({
|
|
114
|
+
documentId: 'doc-1',
|
|
115
|
+
documentType: 'movie',
|
|
116
|
+
// A new array identity on every render, which is what real callers do.
|
|
117
|
+
path: ['title'],
|
|
118
|
+
}),
|
|
119
|
+
{wrapper},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
rerender()
|
|
123
|
+
rerender()
|
|
124
|
+
|
|
125
|
+
expect(reported()).toHaveLength(1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('throttles a burst and announces the final position', () => {
|
|
129
|
+
const {rerender} = renderHook(
|
|
130
|
+
({path}: {path: string[]}) =>
|
|
131
|
+
useReportPresence({documentId: 'doc-1', documentType: 'movie', path}),
|
|
132
|
+
{wrapper, initialProps: {path: ['a']}},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
expect(reported()).toHaveLength(1)
|
|
136
|
+
|
|
137
|
+
rerender({path: ['b']})
|
|
138
|
+
rerender({path: ['c']})
|
|
139
|
+
expect(reported()).toHaveLength(1)
|
|
140
|
+
|
|
141
|
+
// Trailing edge, so the position the user actually settled on is the one sent.
|
|
142
|
+
act(() => {
|
|
143
|
+
vi.advanceTimersByTime(1000)
|
|
144
|
+
})
|
|
145
|
+
expect(reported()).toHaveLength(2)
|
|
146
|
+
expect(reported()[1][0].path).toEqual(['c'])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('announces selections more often than field focus', () => {
|
|
150
|
+
const selectionAt = (offset: number) => ({
|
|
151
|
+
anchor: {path: [{_key: 'b1'}], offset},
|
|
152
|
+
focus: {path: [{_key: 'b1'}], offset},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const {rerender} = renderHook(
|
|
156
|
+
({offset}: {offset: number}) =>
|
|
157
|
+
useReportPresence({
|
|
158
|
+
documentId: 'doc-1',
|
|
159
|
+
documentType: 'movie',
|
|
160
|
+
path: ['body'],
|
|
161
|
+
selection: selectionAt(offset),
|
|
162
|
+
}),
|
|
163
|
+
{wrapper, initialProps: {offset: 1}},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
expect(reported()).toHaveLength(1)
|
|
167
|
+
rerender({offset: 2})
|
|
168
|
+
|
|
169
|
+
// Would still be pending at the 1000ms focus interval. A caret that lags a
|
|
170
|
+
// full second reads as broken, so selections use 250ms.
|
|
171
|
+
act(() => {
|
|
172
|
+
vi.advanceTimersByTime(250)
|
|
173
|
+
})
|
|
174
|
+
expect(reported()).toHaveLength(2)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('clears its location on unmount, staying present but nowhere', () => {
|
|
178
|
+
const {unmount} = renderHook(
|
|
179
|
+
() => useReportPresence({documentId: 'doc-1', documentType: 'movie'}),
|
|
180
|
+
{wrapper},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
unmount()
|
|
184
|
+
|
|
185
|
+
expect(reported().at(-1)).toEqual([])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('does not clear on every location change, only on unmount', () => {
|
|
189
|
+
const {rerender} = renderHook(
|
|
190
|
+
({path}: {path: string[]}) =>
|
|
191
|
+
useReportPresence({documentId: 'doc-1', documentType: 'movie', path}),
|
|
192
|
+
{wrapper, initialProps: {path: ['a']}},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
rerender({path: ['b']})
|
|
196
|
+
act(() => {
|
|
197
|
+
vi.advanceTimersByTime(1000)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
expect(reported().every((locations) => locations.length === 1)).toBe(true)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isMediaLibraryResource,
|
|
3
|
+
type PresenceSelection,
|
|
4
|
+
reportPresence,
|
|
5
|
+
type ReportPresenceOptions,
|
|
6
|
+
} from '@sanity/sdk'
|
|
7
|
+
import {type Path} from '@sanity/types'
|
|
8
|
+
import {useEffect, useMemo, useRef} from 'react'
|
|
9
|
+
|
|
10
|
+
import {type DocumentHandle} from '../../config/handles'
|
|
11
|
+
import {useSanityInstance} from '../context/useSanityInstance'
|
|
12
|
+
import {useNormalizedResourceOptions} from '../helpers/useNormalizedResourceOptions'
|
|
13
|
+
import {trackHookUsage} from '../helpers/useTrackHookUsage'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Field focus moves at human speed, so a second between announcements is plenty.
|
|
17
|
+
*/
|
|
18
|
+
const FOCUS_THROTTLE_MS = 1000
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A caret that trails a second behind reads as broken rather than as latency, so
|
|
22
|
+
* reporting a `selection` announces more often. The Studio uses a flat 1000ms for
|
|
23
|
+
* both, which is why its remote carets feel sluggish.
|
|
24
|
+
*/
|
|
25
|
+
const SELECTION_THROTTLE_MS = 250
|
|
26
|
+
|
|
27
|
+
/** @beta */
|
|
28
|
+
export interface UseReportPresenceOptions extends DocumentHandle {
|
|
29
|
+
/**
|
|
30
|
+
* The focused field path. Omit it for document-level presence. Keyed and numeric
|
|
31
|
+
* segments are supported, so array items and Portable Text spans can be
|
|
32
|
+
* addressed.
|
|
33
|
+
*/
|
|
34
|
+
path?: Path
|
|
35
|
+
|
|
36
|
+
/** The Portable Text caret, when the focused field is a Portable Text field. */
|
|
37
|
+
selection?: PresenceSelection
|
|
38
|
+
|
|
39
|
+
/** Overrides the throttle interval. Mainly useful in tests. */
|
|
40
|
+
throttleMs?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Announces that the current user is in a document, so that other clients in the
|
|
45
|
+
* same project and dataset can show them.
|
|
46
|
+
*
|
|
47
|
+
* Writing presence is opt-in. Reading it with `usePresenceForDocument` or
|
|
48
|
+
* `usePresence` never announces anything, and this hook is the only thing that
|
|
49
|
+
* makes an app visible to others. That includes the Studio, which shares the same
|
|
50
|
+
* presence room and will show these users in its navbar and field indicators.
|
|
51
|
+
*
|
|
52
|
+
* Announcements are throttled, collapsed over a short window, and then repeated
|
|
53
|
+
* every 30 seconds while the user is idle. That repeat is what tells peers the
|
|
54
|
+
* session is still alive, so the intended usage is to mount this hook for as long
|
|
55
|
+
* as the user is in the document. On unmount the location is cleared, leaving the
|
|
56
|
+
* user present in the app but not in any particular document.
|
|
57
|
+
*
|
|
58
|
+
* The perspective decides which specific document is reported: the draft under
|
|
59
|
+
* `drafts`, the published document under `published`, a version under a release.
|
|
60
|
+
* It is taken from `ResourceProvider` unless you pass one on the handle. That
|
|
61
|
+
* matters for interoperability, because the Studio's field indicators compare the
|
|
62
|
+
* exact id its form is on, so a mismatch shows your user at document level while
|
|
63
|
+
* never lighting up a field.
|
|
64
|
+
*
|
|
65
|
+
* Presence is scoped to a single project and dataset. It is not a list of everyone
|
|
66
|
+
* signed in to your organization.
|
|
67
|
+
*
|
|
68
|
+
* @example Document-level presence
|
|
69
|
+
* ```tsx
|
|
70
|
+
* function DocumentEditor({documentId, documentType}: DocumentHandle) {
|
|
71
|
+
* useReportPresence({documentId, documentType})
|
|
72
|
+
* return <Editor />
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example Presence in a release version
|
|
77
|
+
* ```tsx
|
|
78
|
+
* // The document id stays plain; the perspective selects the version.
|
|
79
|
+
* useReportPresence({documentId, documentType, perspective: {releaseName: 'autumn'}})
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example Field-level presence
|
|
83
|
+
* ```tsx
|
|
84
|
+
* function TitleField({documentId, documentType}: DocumentHandle) {
|
|
85
|
+
* const [focused, setFocused] = useState(false)
|
|
86
|
+
* useReportPresence({documentId, documentType, path: focused ? ['title'] : undefined})
|
|
87
|
+
* return <input onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} />
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* @beta
|
|
92
|
+
*/
|
|
93
|
+
export function useReportPresence(options: UseReportPresenceOptions): void {
|
|
94
|
+
const {path, selection, throttleMs, ...handle} = options
|
|
95
|
+
|
|
96
|
+
const normalizedOptions = useNormalizedResourceOptions(handle)
|
|
97
|
+
if (normalizedOptions.resource && isMediaLibraryResource(normalizedOptions.resource)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'useReportPresence() does not support media library resources. Presence tracking requires a canvas or dataset resource.',
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sanityInstance = useSanityInstance()
|
|
104
|
+
trackHookUsage(sanityInstance, 'useReportPresence')
|
|
105
|
+
|
|
106
|
+
const {resource, perspective} = normalizedOptions
|
|
107
|
+
const {documentId, liveEdit} = options
|
|
108
|
+
|
|
109
|
+
const interval = throttleMs ?? (selection ? SELECTION_THROTTLE_MS : FOCUS_THROTTLE_MS)
|
|
110
|
+
|
|
111
|
+
// Compared by value, because callers write `path={['title']}` inline and a fresh
|
|
112
|
+
// array identity every render must not mean a fresh announcement every render.
|
|
113
|
+
const locationKey = useMemo(
|
|
114
|
+
() =>
|
|
115
|
+
JSON.stringify([documentId, path ?? [], selection ?? null, perspective ?? null, liveEdit]),
|
|
116
|
+
[documentId, path, selection, perspective, liveEdit],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// Rebuilt from the key rather than from the props, so the effect below can
|
|
120
|
+
// depend on it honestly instead of suppressing the exhaustive-deps rule.
|
|
121
|
+
const location = useMemo<ReportPresenceOptions>(() => {
|
|
122
|
+
const [id, parsedPath, parsedSelection, parsedPerspective, parsedLiveEdit] = JSON.parse(
|
|
123
|
+
locationKey,
|
|
124
|
+
) as [string, Path, PresenceSelection, ReportPresenceOptions['perspective'] | null, boolean?]
|
|
125
|
+
return {
|
|
126
|
+
documentId: id,
|
|
127
|
+
// Forwarded rather than resolved here: core turns the perspective into the
|
|
128
|
+
// specific document id, so the read and write sides cannot drift apart.
|
|
129
|
+
...(parsedPerspective ? {perspective: parsedPerspective} : {}),
|
|
130
|
+
...(parsedLiveEdit ? {liveEdit: parsedLiveEdit} : {}),
|
|
131
|
+
...(parsedPath.length > 0 ? {path: parsedPath} : {}),
|
|
132
|
+
...(parsedSelection ? {selection: parsedSelection} : {}),
|
|
133
|
+
}
|
|
134
|
+
}, [locationKey])
|
|
135
|
+
|
|
136
|
+
const lastSentAt = useRef(0)
|
|
137
|
+
const pending = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
// Captured in this effect's closure rather than read from a ref, so nothing is
|
|
141
|
+
// written during render. A newer location re-runs the effect, which clears the
|
|
142
|
+
// pending timeout below and schedules the newer value instead.
|
|
143
|
+
const send = () => {
|
|
144
|
+
lastSentAt.current = Date.now()
|
|
145
|
+
reportPresence(sanityInstance, {
|
|
146
|
+
...(resource ? {resource} : {}),
|
|
147
|
+
locations: [location],
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const elapsed = Date.now() - lastSentAt.current
|
|
152
|
+
if (elapsed >= interval) {
|
|
153
|
+
send()
|
|
154
|
+
} else {
|
|
155
|
+
// Trailing edge, so the position the user settled on is the one announced.
|
|
156
|
+
clearTimeout(pending.current)
|
|
157
|
+
pending.current = setTimeout(send, interval - elapsed)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return () => clearTimeout(pending.current)
|
|
161
|
+
}, [location, interval, sanityInstance, resource])
|
|
162
|
+
|
|
163
|
+
// Kept separate from the throttled effect so it runs on unmount only, rather
|
|
164
|
+
// than every time the reported location changes.
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
return () => {
|
|
167
|
+
reportPresence(sanityInstance, {...(resource ? {resource} : {}), locations: []})
|
|
168
|
+
}
|
|
169
|
+
}, [sanityInstance, resource])
|
|
170
|
+
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type PreviewQueryResult,
|
|
4
|
-
type PreviewValue,
|
|
5
|
-
transformProjectionToPreview,
|
|
6
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {type PreviewQueryResult, type PreviewValue} from '@sanity/sdk'
|
|
2
|
+
import {PREVIEW_PROJECTION, transformProjectionToPreview} from '@sanity/sdk/_internal'
|
|
7
3
|
import {useMemo} from 'react'
|
|
8
4
|
|
|
9
5
|
import {type DocumentHandle} from '../../config/handles'
|
|
@@ -2,14 +2,6 @@ import {type Project, projects, type ProjectsOptions} from '@sanity/sdk'
|
|
|
2
2
|
|
|
3
3
|
import {createFetcherHook, type FetcherHookResult} from '../helpers/createFetcherHook'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* @public
|
|
7
|
-
* @category Types
|
|
8
|
-
* @interface
|
|
9
|
-
* @deprecated use the Project type directly.
|
|
10
|
-
*/
|
|
11
|
-
export type ProjectWithoutMembers = Project
|
|
12
|
-
|
|
13
5
|
/**
|
|
14
6
|
* Returns metadata for each project you have access to.
|
|
15
7
|
*
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
getQueryState,
|
|
4
|
-
parseQueryKey,
|
|
5
|
-
type QueryOptions,
|
|
6
|
-
resolveQuery,
|
|
7
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {getQueryState, type QueryOptions, resolveQuery} from '@sanity/sdk'
|
|
2
|
+
import {getQueryKey, parseQueryKey} from '@sanity/sdk/_internal'
|
|
8
3
|
import {type SanityQueryResult} from 'groq'
|
|
9
4
|
import {useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition} from 'react'
|
|
10
5
|
|
|
@@ -162,7 +157,7 @@ export function useQuery(options: WithResourceNameSupport<QueryOptions>): {
|
|
|
162
157
|
const [isPending, startTransition] = useTransition()
|
|
163
158
|
|
|
164
159
|
// Get the unique key for this query and its options (using normalized options)
|
|
165
|
-
const queryKey = getQueryKey(normalized)
|
|
160
|
+
const queryKey = getQueryKey(instance, normalized)
|
|
166
161
|
// Use a deferred state to avoid immediate re-renders when the query changes
|
|
167
162
|
const [deferredQueryKey, setDeferredQueryKey] = useState(queryKey)
|
|
168
163
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type DatasetHandle,
|
|
2
3
|
type DocumentResource,
|
|
3
4
|
getActiveReleasesState,
|
|
4
5
|
type ReleaseDocument,
|
|
5
|
-
type SanityConfig,
|
|
6
6
|
type SanityInstance,
|
|
7
7
|
type StateSource,
|
|
8
8
|
} from '@sanity/sdk'
|
|
@@ -48,7 +48,7 @@ const useActiveReleasesValue: UseActiveReleasesValue = createStateSourceHook({
|
|
|
48
48
|
* ```
|
|
49
49
|
*/
|
|
50
50
|
export function useActiveReleases(
|
|
51
|
-
options?: WithResourceNameSupport<
|
|
51
|
+
options?: WithResourceNameSupport<DatasetHandle> | undefined,
|
|
52
52
|
): ReleaseDocument[] {
|
|
53
53
|
const normalizedOptions = useNormalizedResourceOptions(options ?? {})
|
|
54
54
|
return useActiveReleasesValue(normalizedOptions)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type DatasetHandle,
|
|
2
3
|
type DocumentResource,
|
|
3
4
|
getAllReleasesState,
|
|
4
5
|
type ReleaseDocument,
|
|
5
|
-
type SanityConfig,
|
|
6
6
|
type SanityInstance,
|
|
7
7
|
type StateSource,
|
|
8
8
|
} from '@sanity/sdk'
|
|
@@ -55,7 +55,7 @@ const useAllReleasesValue: UseAllReleasesValue = createStateSourceHook({
|
|
|
55
55
|
* ```
|
|
56
56
|
*/
|
|
57
57
|
export function useAllReleases(
|
|
58
|
-
options?: WithResourceNameSupport<
|
|
58
|
+
options?: WithResourceNameSupport<DatasetHandle> | undefined,
|
|
59
59
|
): ReleaseDocument[] {
|
|
60
60
|
const normalizedOptions = useNormalizedResourceOptions(options ?? {})
|
|
61
61
|
return useAllReleasesValue(normalizedOptions)
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
getUsersKey,
|
|
4
|
-
getUsersState,
|
|
5
|
-
parseUsersKey,
|
|
6
|
-
resolveUsers,
|
|
7
|
-
type SanityUser,
|
|
8
|
-
} from '@sanity/sdk'
|
|
1
|
+
import {type GetUserOptions, getUsersState, resolveUsers, type SanityUser} from '@sanity/sdk'
|
|
2
|
+
import {getUsersKey, parseUsersKey} from '@sanity/sdk/_internal'
|
|
9
3
|
import {useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
|
|
10
4
|
|
|
11
5
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getUsersKey,
|
|
3
2
|
type GetUsersOptions,
|
|
4
3
|
getUsersState,
|
|
5
4
|
loadMoreUsers,
|
|
6
|
-
parseUsersKey,
|
|
7
5
|
resolveUsers,
|
|
8
6
|
type SanityUser,
|
|
9
7
|
} from '@sanity/sdk'
|
|
8
|
+
import {getUsersKey, parseUsersKey} from '@sanity/sdk/_internal'
|
|
10
9
|
import {useCallback, useEffect, useMemo, useState, useSyncExternalStore, useTransition} from 'react'
|
|
11
10
|
|
|
12
11
|
import {useSanityInstance} from '../context/useSanityInstance'
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import {type CreateUserApplicationInput, type UserApplication} from '@sanity/sdk'
|
|
2
|
-
import {expectTypeOf, test} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {type MutationHookResult} from '../helpers/createMutationHook'
|
|
5
|
-
import {useCreateUserApplication} from './useCreateUserApplication'
|
|
6
|
-
|
|
7
|
-
test('useCreateUserApplication — returns the mutation envelope', () => {
|
|
8
|
-
const result = useCreateUserApplication()
|
|
9
|
-
expectTypeOf(result).toEqualTypeOf<
|
|
10
|
-
MutationHookResult<CreateUserApplicationInput, UserApplication>
|
|
11
|
-
>()
|
|
12
|
-
expectTypeOf(result.mutate).parameter(0).toEqualTypeOf<CreateUserApplicationInput>()
|
|
13
|
-
expectTypeOf(result.data).toEqualTypeOf<UserApplication | undefined>()
|
|
14
|
-
})
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import {createUserApplication} from '@sanity/sdk'
|
|
2
|
-
|
|
3
|
-
import {createMutationHook} from '../helpers/createMutationHook'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Creates a user application.
|
|
7
|
-
*
|
|
8
|
-
* @internal
|
|
9
|
-
* @returns The mutation envelope `{mutate, isPending, error, data, reset}`.
|
|
10
|
-
*/
|
|
11
|
-
export const useCreateUserApplication = createMutationHook(createUserApplication)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import {type DeletedResult, type DeleteUserApplicationInput} from '@sanity/sdk'
|
|
2
|
-
import {expectTypeOf, test} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {type MutationHookResult} from '../helpers/createMutationHook'
|
|
5
|
-
import {useDeleteUserApplication} from './useDeleteUserApplication'
|
|
6
|
-
|
|
7
|
-
test('useDeleteUserApplication — returns the mutation envelope', () => {
|
|
8
|
-
const result = useDeleteUserApplication()
|
|
9
|
-
expectTypeOf(result).toEqualTypeOf<
|
|
10
|
-
MutationHookResult<DeleteUserApplicationInput, DeletedResult>
|
|
11
|
-
>()
|
|
12
|
-
expectTypeOf(result.mutate).parameter(0).toEqualTypeOf<DeleteUserApplicationInput>()
|
|
13
|
-
expectTypeOf(result.data).toEqualTypeOf<DeletedResult | undefined>()
|
|
14
|
-
})
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import {deleteUserApplication} from '@sanity/sdk'
|
|
2
|
-
|
|
3
|
-
import {createMutationHook} from '../helpers/createMutationHook'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Deletes a user application.
|
|
7
|
-
*
|
|
8
|
-
* @internal
|
|
9
|
-
* @returns The mutation envelope `{mutate, isPending, error, data, reset}`.
|
|
10
|
-
*/
|
|
11
|
-
export const useDeleteUserApplication = createMutationHook(deleteUserApplication)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import {type UpdateUserApplicationInput, type UserApplication} from '@sanity/sdk'
|
|
2
|
-
import {expectTypeOf, test} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {type MutationHookResult} from '../helpers/createMutationHook'
|
|
5
|
-
import {useUpdateUserApplication} from './useUpdateUserApplication'
|
|
6
|
-
|
|
7
|
-
test('useUpdateUserApplication — returns the mutation envelope', () => {
|
|
8
|
-
const result = useUpdateUserApplication()
|
|
9
|
-
expectTypeOf(result).toEqualTypeOf<
|
|
10
|
-
MutationHookResult<UpdateUserApplicationInput, UserApplication>
|
|
11
|
-
>()
|
|
12
|
-
expectTypeOf(result.mutate).parameter(0).toEqualTypeOf<UpdateUserApplicationInput>()
|
|
13
|
-
expectTypeOf(result.data).toEqualTypeOf<UserApplication | undefined>()
|
|
14
|
-
})
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import {updateUserApplication} from '@sanity/sdk'
|
|
2
|
-
|
|
3
|
-
import {createMutationHook} from '../helpers/createMutationHook'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Updates a user application.
|
|
7
|
-
*
|
|
8
|
-
* @internal
|
|
9
|
-
* @returns The mutation envelope `{mutate, isPending, error, data, reset}`.
|
|
10
|
-
*/
|
|
11
|
-
export const useUpdateUserApplication = createMutationHook(updateUserApplication)
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import {type UserApplication} from '@sanity/sdk'
|
|
2
|
-
import {expectTypeOf, test} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {type FetcherHookResult} from '../helpers/createFetcherHook'
|
|
5
|
-
import {useUserApplication} from './useUserApplication'
|
|
6
|
-
|
|
7
|
-
test('useUserApplication — returns a single user application in the result envelope', () => {
|
|
8
|
-
const result = useUserApplication('user_app_1')
|
|
9
|
-
expectTypeOf(result).toEqualTypeOf<FetcherHookResult<UserApplication>>()
|
|
10
|
-
expectTypeOf(result.data).toEqualTypeOf<UserApplication>()
|
|
11
|
-
})
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import {userApplication} from '@sanity/sdk'
|
|
2
|
-
|
|
3
|
-
import {createFetcherHook} from '../helpers/createFetcherHook'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Returns a single user application by id.
|
|
7
|
-
*
|
|
8
|
-
* The hook suspends until the first fetch succeeds, so `data` is always present.
|
|
9
|
-
*
|
|
10
|
-
* @public
|
|
11
|
-
* @param userApplicationId - The user application id.
|
|
12
|
-
* @returns The result envelope `{data, isFetching, error, refetch}`.
|
|
13
|
-
*/
|
|
14
|
-
export const useUserApplication = createFetcherHook(userApplication)
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import {type UserApplication} from '@sanity/sdk'
|
|
2
|
-
import {expectTypeOf, test} from 'vitest'
|
|
3
|
-
|
|
4
|
-
import {type FetcherHookResult} from '../helpers/createFetcherHook'
|
|
5
|
-
import {useUserApplications} from './useUserApplications'
|
|
6
|
-
|
|
7
|
-
test('useUserApplications — returns the user applications in the result envelope', () => {
|
|
8
|
-
const result = useUserApplications({organizationId: 'org_1'})
|
|
9
|
-
expectTypeOf(result).toEqualTypeOf<FetcherHookResult<UserApplication[]>>()
|
|
10
|
-
expectTypeOf(result.data).toEqualTypeOf<UserApplication[]>()
|
|
11
|
-
})
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import {userApplications} from '@sanity/sdk'
|
|
2
|
-
|
|
3
|
-
import {createFetcherHook} from '../helpers/createFetcherHook'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Returns the current user's applications for the given organisation.
|
|
7
|
-
*
|
|
8
|
-
* The hook suspends until the first fetch succeeds, so `data` is always present.
|
|
9
|
-
*
|
|
10
|
-
* @public
|
|
11
|
-
* @param options - Options identifying the organisation.
|
|
12
|
-
* @returns The result envelope `{data, isFetching, error, refetch}`.
|
|
13
|
-
*/
|
|
14
|
-
export const useUserApplications = createFetcherHook(userApplications)
|