@sanity/sdk 2.9.0 → 2.10.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/_chunks-dts/utils.d.ts +105 -51
- package/dist/_chunks-es/createGroqSearchFilter.js +131 -54
- package/dist/_chunks-es/createGroqSearchFilter.js.map +1 -1
- package/dist/_chunks-es/version.js +1 -1
- package/dist/_exports/_internal.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +119 -73
- package/dist/index.js.map +1 -1
- package/package.json +8 -10
- package/src/_exports/index.ts +8 -0
- package/src/client/clientStore.test.ts +30 -30
- package/src/client/clientStore.ts +47 -47
- package/src/comlink/controller/actions/getOrCreateChannel.ts +2 -2
- package/src/comlink/node/actions/getOrCreateNode.ts +2 -2
- package/src/config/sanityConfig.ts +72 -12
- package/src/document/applyDocumentActions.test.ts +7 -7
- package/src/document/applyDocumentActions.ts +5 -5
- package/src/document/documentStore.test.ts +68 -62
- package/src/document/documentStore.ts +36 -36
- package/src/document/processActions.ts +2 -2
- package/src/document/reducers.ts +4 -4
- package/src/document/sharedListener.ts +7 -7
- package/src/presence/bifurTransport.test.ts +46 -6
- package/src/presence/bifurTransport.ts +13 -1
- package/src/presence/presenceStore.test.ts +96 -0
- package/src/presence/presenceStore.ts +96 -24
- package/src/preview/getPreviewState.ts +1 -1
- package/src/preview/previewProjectionUtils.test.ts +4 -4
- package/src/preview/previewProjectionUtils.ts +7 -7
- package/src/preview/resolvePreview.ts +5 -1
- package/src/projection/getProjectionState.ts +4 -4
- package/src/projection/projectionStore.test.ts +2 -2
- package/src/projection/resolveProjection.ts +2 -2
- package/src/projection/subscribeToStateAndFetchBatches.test.ts +1 -1
- package/src/projection/subscribeToStateAndFetchBatches.ts +12 -11
- package/src/query/queryStore.test.ts +12 -12
- package/src/query/queryStore.ts +10 -10
- package/src/query/reducers.ts +3 -3
- package/src/releases/getPerspectiveState.ts +5 -5
- package/src/releases/releasesStore.test.ts +6 -6
- package/src/releases/releasesStore.ts +9 -9
- package/src/store/createActionBinder.test.ts +31 -31
- package/src/store/createActionBinder.ts +43 -38
- package/src/store/createSanityInstance.ts +2 -3
- package/src/users/reducers.ts +3 -4
- package/src/utils/createFetcherStore.ts +6 -4
- package/src/utils/isImportError.test.ts +72 -0
- package/src/utils/isImportError.ts +34 -0
- package/src/utils/object.test.ts +95 -0
- package/src/utils/object.ts +142 -0
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
type SanityDocument,
|
|
8
8
|
} from '@sanity/types'
|
|
9
9
|
import {evaluateSync, type ExprNode} from 'groq-js'
|
|
10
|
-
import {isEqual} from 'lodash-es'
|
|
11
10
|
|
|
12
11
|
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
12
|
+
import {isDeepEqual} from '../utils/object'
|
|
13
13
|
import {type DocumentAction} from './actions'
|
|
14
14
|
import {type Grant} from './permissions'
|
|
15
15
|
import {type DocumentSet, getId, processMutations} from './processMutations'
|
|
@@ -568,7 +568,7 @@ export function processActions({
|
|
|
568
568
|
|
|
569
569
|
// Before proceeding, verify that the working draft is identical to the base draft.
|
|
570
570
|
// TODO: is it enough just to check for the _rev or nah?
|
|
571
|
-
if (!
|
|
571
|
+
if (!isDeepEqual(workingDraft, baseDraft)) {
|
|
572
572
|
throw new ActionError({
|
|
573
573
|
documentId,
|
|
574
574
|
transactionId,
|
package/src/document/reducers.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {DocumentId, getDraftId, getPublishedId, getVersionId} from '@sanity/id-utils'
|
|
2
2
|
import {type Mutation, type PatchOperations, type SanityDocumentLike} from '@sanity/types'
|
|
3
|
-
import {omit} from 'lodash-es'
|
|
4
3
|
|
|
5
4
|
import {type DocumentHandle} from '../config/sanityConfig'
|
|
6
5
|
import {isReleasePerspective} from '../releases/utils/isReleasePerspective'
|
|
7
6
|
import {type StoreContext} from '../store/defineStore'
|
|
8
7
|
import {insecureRandomId} from '../utils/ids'
|
|
8
|
+
import {omitProperty} from '../utils/object'
|
|
9
9
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
10
10
|
import {type DocumentAction} from './actions'
|
|
11
11
|
import {DOCUMENT_STATE_CLEAR_DELAY} from './documentConstants'
|
|
@@ -392,7 +392,7 @@ export function revertOutgoingTransaction(prev: SyncTransactionState): SyncTrans
|
|
|
392
392
|
local: documentId in working ? working[documentId] : local,
|
|
393
393
|
unverifiedRevisions:
|
|
394
394
|
prev.outgoing && prev.outgoing.transactionId in unverifiedRevisions
|
|
395
|
-
?
|
|
395
|
+
? omitProperty(unverifiedRevisions, prev.outgoing.transactionId)
|
|
396
396
|
: unverifiedRevisions,
|
|
397
397
|
}
|
|
398
398
|
return [documentId, next] as const
|
|
@@ -420,7 +420,7 @@ export function applyRemoteDocument(
|
|
|
420
420
|
const revisionToVerify = revision ? prevUnverifiedRevisions?.[revision] : undefined
|
|
421
421
|
let unverifiedRevisions = prevUnverifiedRevisions ?? EMPTY_REVISIONS
|
|
422
422
|
if (revision && revisionToVerify) {
|
|
423
|
-
unverifiedRevisions =
|
|
423
|
+
unverifiedRevisions = omitProperty(prevUnverifiedRevisions, revision)
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
// if this remote document is from a `'sync'` event (meaning that the whole
|
|
@@ -547,7 +547,7 @@ export function removeSubscriptionIdFromDocument(
|
|
|
547
547
|
|
|
548
548
|
if (!prevDocState) return prev
|
|
549
549
|
if (!subscriptions.length) {
|
|
550
|
-
return {...prev, documentStates:
|
|
550
|
+
return {...prev, documentStates: omitProperty(prev.documentStates, documentId)}
|
|
551
551
|
}
|
|
552
552
|
return {
|
|
553
553
|
...prev,
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from 'rxjs'
|
|
15
15
|
|
|
16
16
|
import {getClientState} from '../client/clientStore'
|
|
17
|
-
import {type
|
|
17
|
+
import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
|
|
18
18
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
19
19
|
|
|
20
20
|
const API_VERSION = 'v2025-05-06'
|
|
@@ -26,13 +26,13 @@ export interface SharedListener {
|
|
|
26
26
|
|
|
27
27
|
export function createSharedListener(
|
|
28
28
|
instance: SanityInstance,
|
|
29
|
-
|
|
29
|
+
resource?: DocumentResource,
|
|
30
30
|
): SharedListener {
|
|
31
31
|
const dispose$ = new Subject<void>()
|
|
32
32
|
const events$ = getClientState(instance, {
|
|
33
33
|
apiVersion: API_VERSION,
|
|
34
|
-
// TODO: remove in v3 when we're ready for everything to be queried via
|
|
35
|
-
|
|
34
|
+
// TODO: remove in v3 when we're ready for everything to be queried via resource
|
|
35
|
+
resource: resource && !isDatasetResource(resource) ? resource : undefined,
|
|
36
36
|
}).observable.pipe(
|
|
37
37
|
switchMap((client) =>
|
|
38
38
|
// TODO: it seems like the client.listen method is not emitting disconnected
|
|
@@ -68,12 +68,12 @@ export function createSharedListener(
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
export function createFetchDocument(instance: SanityInstance,
|
|
71
|
+
export function createFetchDocument(instance: SanityInstance, resource?: DocumentResource) {
|
|
72
72
|
return function (documentId: string): Observable<SanityDocument | null> {
|
|
73
73
|
return getClientState(instance, {
|
|
74
74
|
apiVersion: API_VERSION,
|
|
75
|
-
// TODO: remove in v3 when we're ready for everything to be queried via
|
|
76
|
-
|
|
75
|
+
// TODO: remove in v3 when we're ready for everything to be queried via resource
|
|
76
|
+
resource: resource && !isDatasetResource(resource) ? resource : undefined,
|
|
77
77
|
}).observable.pipe(
|
|
78
78
|
switchMap((client) => {
|
|
79
79
|
// creates a observable request to the /doc/{documentId} endpoint for a given document id
|
|
@@ -45,16 +45,18 @@ describe('createBifurTransport', () => {
|
|
|
45
45
|
|
|
46
46
|
beforeEach(() => {
|
|
47
47
|
vi.useFakeTimers()
|
|
48
|
+
vi.clearAllMocks()
|
|
48
49
|
mockBifurClient = {
|
|
49
50
|
listen: vi.fn(() => new Subject<never>()),
|
|
50
51
|
request: vi.fn(() => of(undefined)),
|
|
51
52
|
}
|
|
52
53
|
fromUrlMock.mockReturnValue(mockBifurClient)
|
|
53
54
|
|
|
55
|
+
// Default mock is a dataset client using project hostname
|
|
54
56
|
mockSanityClient = {
|
|
55
57
|
config: () => ({
|
|
56
58
|
dataset: 'test-dataset',
|
|
57
|
-
url: '
|
|
59
|
+
url: 'https://test-project.api.sanity.io/v2022-06-30',
|
|
58
60
|
requestTagPrefix: 'test-tag',
|
|
59
61
|
}),
|
|
60
62
|
withConfig: vi.fn().mockReturnThis(),
|
|
@@ -63,7 +65,7 @@ describe('createBifurTransport', () => {
|
|
|
63
65
|
token$ = new Subject<string | null>()
|
|
64
66
|
})
|
|
65
67
|
|
|
66
|
-
it('constructs the bifur client
|
|
68
|
+
it('constructs the bifur client URL for a dataset resource', () => {
|
|
67
69
|
createBifurTransport({
|
|
68
70
|
client: mockSanityClient,
|
|
69
71
|
token$,
|
|
@@ -71,13 +73,51 @@ describe('createBifurTransport', () => {
|
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
expect(fromUrlMock).toHaveBeenCalledWith(
|
|
74
|
-
'
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
'wss://test-project.api.sanity.io/v2022-06-30/socket/test-dataset?tag=test-tag',
|
|
77
|
+
{token$},
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('constructs the bifur client URL for a canvas resource', () => {
|
|
82
|
+
const canvasClient = {
|
|
83
|
+
config: () => ({
|
|
84
|
+
resource: {type: 'canvas', id: 'canvas-123'},
|
|
85
|
+
url: 'https://api.sanity.io/v2022-06-30',
|
|
86
|
+
requestTagPrefix: 'test-tag',
|
|
87
|
+
}),
|
|
88
|
+
withConfig: vi.fn().mockReturnThis(),
|
|
89
|
+
} as unknown as SanityClient
|
|
90
|
+
|
|
91
|
+
createBifurTransport({
|
|
92
|
+
client: canvasClient,
|
|
93
|
+
token$,
|
|
94
|
+
sessionId: 'session-id-123',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(fromUrlMock).toHaveBeenCalledWith(
|
|
98
|
+
'wss://api.sanity.io/v2022-06-30/socket/canvases/canvas-123?tag=test-tag',
|
|
99
|
+
{token$},
|
|
78
100
|
)
|
|
79
101
|
})
|
|
80
102
|
|
|
103
|
+
it('throws when no canvas resource or dataset is configured', () => {
|
|
104
|
+
const invalidClient = {
|
|
105
|
+
config: () => ({
|
|
106
|
+
url: 'https://api.sanity.io/v2022-06-30',
|
|
107
|
+
requestTagPrefix: 'test-tag',
|
|
108
|
+
}),
|
|
109
|
+
withConfig: vi.fn().mockReturnThis(),
|
|
110
|
+
} as unknown as SanityClient
|
|
111
|
+
|
|
112
|
+
expect(() =>
|
|
113
|
+
createBifurTransport({
|
|
114
|
+
client: invalidClient,
|
|
115
|
+
token$,
|
|
116
|
+
sessionId: 'session-id-123',
|
|
117
|
+
}),
|
|
118
|
+
).toThrow('Unable to determine presence URL')
|
|
119
|
+
})
|
|
120
|
+
|
|
81
121
|
it('handles incoming rollCall events', () => {
|
|
82
122
|
const incomingBifurEvents$ = new Subject<IncomingBifurEvent>()
|
|
83
123
|
mockBifurClient.listen.mockReturnValue(incomingBifurEvents$)
|
|
@@ -37,11 +37,23 @@ type IncomingBifurEvent = RollCallEvent | BifurStateMessage | BifurDisconnectMes
|
|
|
37
37
|
function getBifurClient(client: SanityClient, token$: Observable<string | null>): BifurClient {
|
|
38
38
|
const bifurVersionedClient = client.withConfig({apiVersion: '2022-06-30'})
|
|
39
39
|
const {
|
|
40
|
+
resource,
|
|
40
41
|
dataset,
|
|
41
42
|
url: baseUrl,
|
|
42
43
|
requestTagPrefix = 'sanity.sdk.presence',
|
|
43
44
|
} = bifurVersionedClient.config()
|
|
44
|
-
|
|
45
|
+
|
|
46
|
+
let resourcePath: string
|
|
47
|
+
if (resource?.type === 'canvas') {
|
|
48
|
+
resourcePath = `canvases/${resource.id}`
|
|
49
|
+
} else if (dataset) {
|
|
50
|
+
// Dataset clients use project hostname — dataset name alone is the socket path
|
|
51
|
+
resourcePath = dataset
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Unable to determine presence URL: no canvas resource or dataset configured`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const url = `${baseUrl}/socket/${resourcePath}`.replace(/^http/, 'ws')
|
|
45
57
|
|
|
46
58
|
const urlWithTag = `${url}?tag=${requestTagPrefix}`
|
|
47
59
|
|
|
@@ -48,6 +48,9 @@ describe('presenceStore', () => {
|
|
|
48
48
|
|
|
49
49
|
mockClient = {
|
|
50
50
|
withConfig: vi.fn().mockReturnThis(),
|
|
51
|
+
observable: {
|
|
52
|
+
request: vi.fn(() => of({organizationId: 'test-org-id'})),
|
|
53
|
+
},
|
|
51
54
|
} as unknown as SanityClient
|
|
52
55
|
|
|
53
56
|
mockTokenState = new Subject<string | null>()
|
|
@@ -243,5 +246,98 @@ describe('presenceStore', () => {
|
|
|
243
246
|
|
|
244
247
|
unsubscribe()
|
|
245
248
|
})
|
|
249
|
+
|
|
250
|
+
it('should throw an error when initialized with a media library resource', () => {
|
|
251
|
+
const mediaLibraryResource = {mediaLibraryId: 'ml123'}
|
|
252
|
+
|
|
253
|
+
expect(() => {
|
|
254
|
+
getPresence(instance, {resource: mediaLibraryResource})
|
|
255
|
+
}).toThrow('Presence is not supported for media library resources.')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should work with a dataset resource', () => {
|
|
259
|
+
const datasetResource = {projectId: 'test-project', dataset: 'test-dataset'}
|
|
260
|
+
|
|
261
|
+
expect(() => {
|
|
262
|
+
getPresence(instance, {resource: datasetResource})
|
|
263
|
+
}).not.toThrow()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should work with a canvas resource', () => {
|
|
267
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
268
|
+
|
|
269
|
+
expect(() => {
|
|
270
|
+
getPresence(instance, {resource: canvasResource})
|
|
271
|
+
}).not.toThrow()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('creates a project-hostname client for dataset resources', () => {
|
|
275
|
+
getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
|
|
276
|
+
|
|
277
|
+
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
278
|
+
apiVersion: '2026-03-30',
|
|
279
|
+
projectId: 'my-project',
|
|
280
|
+
dataset: 'my-dataset',
|
|
281
|
+
useProjectHostname: true,
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('creates a resource client for canvas resources', () => {
|
|
286
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
287
|
+
getPresence(instance, {resource: canvasResource})
|
|
288
|
+
|
|
289
|
+
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
290
|
+
apiVersion: '2026-03-30',
|
|
291
|
+
resource: canvasResource,
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('fetches organizationId from canvas endpoint for canvas resources', () => {
|
|
296
|
+
const canvasResource = {canvasId: 'canvas123'}
|
|
297
|
+
getPresence(instance, {resource: canvasResource})
|
|
298
|
+
|
|
299
|
+
expect(mockClient.observable.request).toHaveBeenCalledWith({
|
|
300
|
+
uri: '/canvases/canvas123',
|
|
301
|
+
tag: 'canvases.get',
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('does not fetch organizationId for dataset resources', () => {
|
|
306
|
+
getPresence(instance, {resource: {projectId: 'my-project', dataset: 'my-dataset'}})
|
|
307
|
+
|
|
308
|
+
expect(mockClient.observable.request).not.toHaveBeenCalled()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('fetches user data for canvas users', async () => {
|
|
312
|
+
const source = getPresence(instance, {resource: {canvasId: 'canvas123'}})
|
|
313
|
+
const unsubscribe = source.subscribe(() => {})
|
|
314
|
+
|
|
315
|
+
await firstValueFrom(of(null).pipe(delay(10)))
|
|
316
|
+
|
|
317
|
+
mockIncomingEvents.next({
|
|
318
|
+
type: 'state',
|
|
319
|
+
userId: 'user-1',
|
|
320
|
+
sessionId: 'other-session',
|
|
321
|
+
timestamp: '2023-01-01T12:00:00Z',
|
|
322
|
+
locations: [
|
|
323
|
+
{
|
|
324
|
+
type: 'document',
|
|
325
|
+
documentId: 'doc-1',
|
|
326
|
+
path: ['title'],
|
|
327
|
+
lastActiveAt: '2023-01-01T12:00:00Z',
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
await firstValueFrom(of(null).pipe(delay(50)))
|
|
333
|
+
|
|
334
|
+
expect(getUserState).toHaveBeenCalledWith(instance, {
|
|
335
|
+
userId: 'user-1',
|
|
336
|
+
resourceType: 'organization',
|
|
337
|
+
organizationId: 'test-org-id',
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
unsubscribe()
|
|
341
|
+
})
|
|
246
342
|
})
|
|
247
343
|
})
|
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
import {type SanityClient} from '@sanity/client'
|
|
2
1
|
import {createSelector} from 'reselect'
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
catchError,
|
|
4
|
+
combineLatest,
|
|
5
|
+
distinctUntilChanged,
|
|
6
|
+
EMPTY,
|
|
7
|
+
filter,
|
|
8
|
+
first,
|
|
9
|
+
map,
|
|
10
|
+
type Observable,
|
|
11
|
+
of,
|
|
12
|
+
Subscription,
|
|
13
|
+
switchMap,
|
|
14
|
+
} from 'rxjs'
|
|
4
15
|
|
|
5
16
|
import {getTokenState} from '../auth/authStore'
|
|
6
17
|
import {getClient} from '../client/clientStore'
|
|
7
|
-
import {
|
|
8
|
-
|
|
18
|
+
import {
|
|
19
|
+
type DocumentResource,
|
|
20
|
+
isCanvasResource,
|
|
21
|
+
isDatasetResource,
|
|
22
|
+
isMediaLibraryResource,
|
|
23
|
+
} from '../config/sanityConfig'
|
|
24
|
+
import {bindActionByResource, type BoundResourceKey} from '../store/createActionBinder'
|
|
25
|
+
import {type SanityInstance} from '../store/createSanityInstance'
|
|
26
|
+
import {
|
|
27
|
+
createStateSourceAction,
|
|
28
|
+
type SelectorContext,
|
|
29
|
+
type StateSource,
|
|
30
|
+
} from '../store/createStateSourceAction'
|
|
9
31
|
import {defineStore, type StoreContext} from '../store/defineStore'
|
|
10
32
|
import {type SanityUser} from '../users/types'
|
|
11
33
|
import {getUserState} from '../users/usersStore'
|
|
12
34
|
import {createBifurTransport} from './bifurTransport'
|
|
13
35
|
import {type PresenceLocation, type TransportEvent, type UserPresence} from './types'
|
|
14
36
|
|
|
37
|
+
const PRESENCE_API_VERSION = '2026-03-30'
|
|
38
|
+
|
|
15
39
|
type PresenceStoreState = {
|
|
16
40
|
locations: Map<string, {userId: string; locations: PresenceLocation[]}>
|
|
17
41
|
users: Record<string, SanityUser | undefined>
|
|
42
|
+
organizationId?: string
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
const getInitialState = (): PresenceStoreState => ({
|
|
@@ -23,27 +48,40 @@ const getInitialState = (): PresenceStoreState => ({
|
|
|
23
48
|
})
|
|
24
49
|
|
|
25
50
|
/** @public */
|
|
26
|
-
export const presenceStore = defineStore<PresenceStoreState,
|
|
51
|
+
export const presenceStore = defineStore<PresenceStoreState, BoundResourceKey>({
|
|
27
52
|
name: 'presence',
|
|
28
53
|
getInitialState,
|
|
29
|
-
initialize: (context: StoreContext<PresenceStoreState,
|
|
54
|
+
initialize: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
|
|
30
55
|
const {
|
|
31
56
|
instance,
|
|
32
57
|
state,
|
|
33
|
-
key: {
|
|
58
|
+
key: {resource},
|
|
34
59
|
} = context
|
|
60
|
+
|
|
61
|
+
if (isMediaLibraryResource(resource)) {
|
|
62
|
+
throw new Error('Presence is not supported for media library resources.')
|
|
63
|
+
}
|
|
64
|
+
|
|
35
65
|
const sessionId = crypto.randomUUID()
|
|
36
66
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
67
|
+
// Dataset resources must use the project hostname so the socket URL is project-specific.
|
|
68
|
+
// Canvas resources use the global API endpoint via the resource config.
|
|
69
|
+
const client = isDatasetResource(resource)
|
|
70
|
+
? getClient(instance, {
|
|
71
|
+
apiVersion: PRESENCE_API_VERSION,
|
|
72
|
+
projectId: resource.projectId,
|
|
73
|
+
dataset: resource.dataset,
|
|
74
|
+
useProjectHostname: true,
|
|
75
|
+
})
|
|
76
|
+
: getClient(instance, {
|
|
77
|
+
apiVersion: PRESENCE_API_VERSION,
|
|
78
|
+
resource,
|
|
79
|
+
})
|
|
42
80
|
|
|
43
81
|
const token$ = getTokenState(instance).observable.pipe(distinctUntilChanged())
|
|
44
82
|
|
|
45
83
|
const [incomingEvents$, dispatch] = createBifurTransport({
|
|
46
|
-
client
|
|
84
|
+
client,
|
|
47
85
|
token$,
|
|
48
86
|
sessionId,
|
|
49
87
|
})
|
|
@@ -81,6 +119,22 @@ export const presenceStore = defineStore<PresenceStoreState, BoundDatasetKey>({
|
|
|
81
119
|
|
|
82
120
|
dispatch({type: 'rollCall'}).subscribe()
|
|
83
121
|
|
|
122
|
+
// Canvas resources need the organizationId to resolve users — fetch it once from the canvas endpoint
|
|
123
|
+
if (isCanvasResource(resource)) {
|
|
124
|
+
const globalClient = getClient(instance, {apiVersion: PRESENCE_API_VERSION})
|
|
125
|
+
subscription.add(
|
|
126
|
+
globalClient.observable
|
|
127
|
+
.request<{organizationId: string}>({
|
|
128
|
+
uri: `/canvases/${resource.canvasId}`,
|
|
129
|
+
tag: 'canvases.get',
|
|
130
|
+
})
|
|
131
|
+
.pipe(catchError(() => EMPTY))
|
|
132
|
+
.subscribe(({organizationId}) => {
|
|
133
|
+
state.set('presence/organizationId', (prev) => ({...prev, organizationId}))
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
84
138
|
return () => {
|
|
85
139
|
dispatch({type: 'disconnect'}).subscribe()
|
|
86
140
|
subscription.unsubscribe()
|
|
@@ -114,13 +168,13 @@ const selectPresence = createSelector(
|
|
|
114
168
|
},
|
|
115
169
|
)
|
|
116
170
|
|
|
117
|
-
|
|
118
|
-
export const getPresence = bindActionByDataset(
|
|
171
|
+
const _getPresence = bindActionByResource(
|
|
119
172
|
presenceStore,
|
|
120
173
|
createStateSourceAction({
|
|
121
|
-
selector: (context: SelectorContext<PresenceStoreState
|
|
174
|
+
selector: (context: SelectorContext<PresenceStoreState>): UserPresence[] =>
|
|
122
175
|
selectPresence(context.state),
|
|
123
|
-
onSubscribe: (context: StoreContext<PresenceStoreState,
|
|
176
|
+
onSubscribe: (context: StoreContext<PresenceStoreState, BoundResourceKey>) => {
|
|
177
|
+
const resource = context.key.resource
|
|
124
178
|
const userIds$ = context.state.observable.pipe(
|
|
125
179
|
map((state) =>
|
|
126
180
|
Array.from(state.locations.values())
|
|
@@ -130,26 +184,34 @@ export const getPresence = bindActionByDataset(
|
|
|
130
184
|
distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])),
|
|
131
185
|
)
|
|
132
186
|
|
|
133
|
-
|
|
187
|
+
// For canvas resources, wait for organizationId to be fetched and stored in state.
|
|
188
|
+
// For dataset resources, emit undefined immediately so the stream isn't blocked.
|
|
189
|
+
const organizationId$: Observable<string | undefined> = isCanvasResource(resource)
|
|
190
|
+
? context.state.observable.pipe(
|
|
191
|
+
map((s) => s.organizationId),
|
|
192
|
+
filter((id): id is string => id !== undefined),
|
|
193
|
+
first(),
|
|
194
|
+
)
|
|
195
|
+
: of(undefined)
|
|
196
|
+
|
|
197
|
+
const subscription = combineLatest([userIds$, organizationId$])
|
|
134
198
|
.pipe(
|
|
135
|
-
switchMap((userIds) => {
|
|
199
|
+
switchMap(([userIds, organizationId]) => {
|
|
136
200
|
if (userIds.length === 0) {
|
|
137
201
|
return of([])
|
|
138
202
|
}
|
|
139
203
|
const userObservables = userIds.map((userId) =>
|
|
140
204
|
getUserState(context.instance, {
|
|
141
205
|
userId,
|
|
142
|
-
|
|
143
|
-
|
|
206
|
+
...(isDatasetResource(resource)
|
|
207
|
+
? {resourceType: 'project', projectId: resource.projectId}
|
|
208
|
+
: {resourceType: 'organization', organizationId}),
|
|
144
209
|
}).pipe(filter((v): v is NonNullable<typeof v> => !!v)),
|
|
145
210
|
)
|
|
146
211
|
return combineLatest(userObservables)
|
|
147
212
|
}),
|
|
148
213
|
)
|
|
149
214
|
.subscribe((users) => {
|
|
150
|
-
if (!users) {
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
215
|
context.state.set('presence/users', (prevState) => ({
|
|
154
216
|
...prevState,
|
|
155
217
|
users: {
|
|
@@ -167,3 +229,13 @@ export const getPresence = bindActionByDataset(
|
|
|
167
229
|
},
|
|
168
230
|
}),
|
|
169
231
|
)
|
|
232
|
+
|
|
233
|
+
/** @beta */
|
|
234
|
+
export function getPresence(
|
|
235
|
+
instance: SanityInstance,
|
|
236
|
+
params?: {resource?: DocumentResource},
|
|
237
|
+
): StateSource<UserPresence[]> {
|
|
238
|
+
// bit of a hack to support the old bound action by dataset
|
|
239
|
+
// in reality, this will always be passed a resource
|
|
240
|
+
return _getPresence(instance, params ?? {})
|
|
241
|
+
}
|
|
@@ -52,7 +52,7 @@ export function getPreviewState(
|
|
|
52
52
|
return {data: null, isPending: current?.isPending ?? false}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const previewValue = transformProjectionToPreview(instance, current.data, options.
|
|
55
|
+
const previewValue = transformProjectionToPreview(instance, current.data, options.resource)
|
|
56
56
|
|
|
57
57
|
return {
|
|
58
58
|
data: previewValue,
|
|
@@ -156,7 +156,7 @@ describe('transformProjectionToPreview', () => {
|
|
|
156
156
|
})
|
|
157
157
|
})
|
|
158
158
|
|
|
159
|
-
it('calls getClient with the provided
|
|
159
|
+
it('calls getClient with the provided resource', async () => {
|
|
160
160
|
const projectionResult: PreviewQueryResult = {
|
|
161
161
|
_id: 'doc1',
|
|
162
162
|
_type: 'article',
|
|
@@ -166,14 +166,14 @@ describe('transformProjectionToPreview', () => {
|
|
|
166
166
|
media: null,
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
const
|
|
169
|
+
const resource = {mediaLibraryId: 'test-library'}
|
|
170
170
|
|
|
171
|
-
transformProjectionToPreview(instance, projectionResult,
|
|
171
|
+
transformProjectionToPreview(instance, projectionResult, resource)
|
|
172
172
|
|
|
173
173
|
const {getClient} = await import('../client/clientStore')
|
|
174
174
|
expect(getClient).toHaveBeenCalledWith(instance, {
|
|
175
175
|
apiVersion: 'v2025-05-06',
|
|
176
|
-
|
|
176
|
+
resource,
|
|
177
177
|
})
|
|
178
178
|
})
|
|
179
179
|
})
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {type SanityClient} from '@sanity/client'
|
|
2
2
|
import {createImageUrlBuilder} from '@sanity/image-url'
|
|
3
|
-
import {isObject} from 'lodash-es'
|
|
4
3
|
|
|
5
4
|
import {getClient} from '../client/clientStore'
|
|
6
|
-
import {type
|
|
5
|
+
import {type DocumentResource, isDatasetResource} from '../config/sanityConfig'
|
|
7
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
7
|
+
import {isObject} from '../utils/object'
|
|
8
8
|
import {SUBTITLE_CANDIDATES, TITLE_CANDIDATES} from './previewConstants'
|
|
9
9
|
import {type PreviewQueryResult, type PreviewValue} from './types'
|
|
10
10
|
|
|
@@ -66,22 +66,22 @@ function findFirstDefined(
|
|
|
66
66
|
*
|
|
67
67
|
* @param projectionResult - The raw projection result from GROQ
|
|
68
68
|
* @param instance - The Sanity instance to use for client configuration
|
|
69
|
-
* @param
|
|
69
|
+
* @param resource - Data resource for the preview
|
|
70
70
|
* @internal
|
|
71
71
|
*/
|
|
72
72
|
export function transformProjectionToPreview(
|
|
73
73
|
instance: SanityInstance,
|
|
74
74
|
projectionResult: PreviewQueryResult,
|
|
75
|
-
|
|
75
|
+
resource?: DocumentResource,
|
|
76
76
|
): PreviewValue {
|
|
77
77
|
const title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates)
|
|
78
78
|
const subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title)
|
|
79
79
|
|
|
80
|
-
// Get a client for the
|
|
80
|
+
// Get a client for the resource (if provided) or use the instance config
|
|
81
81
|
const client = getClient(instance, {
|
|
82
82
|
apiVersion: API_VERSION,
|
|
83
|
-
// TODO: remove in v3 when we're ready for everything to be queried via
|
|
84
|
-
|
|
83
|
+
// TODO: remove in v3 when we're ready for everything to be queried via resource
|
|
84
|
+
resource: resource && !isDatasetResource(resource) ? resource : undefined,
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
return {
|
|
@@ -30,7 +30,11 @@ export async function resolvePreview(
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
// Transform to preview format
|
|
33
|
-
const previewValue = transformProjectionToPreview(
|
|
33
|
+
const previewValue = transformProjectionToPreview(
|
|
34
|
+
instance,
|
|
35
|
+
projectionResult.data,
|
|
36
|
+
options.resource,
|
|
37
|
+
)
|
|
34
38
|
|
|
35
39
|
return {
|
|
36
40
|
data: previewValue,
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {DocumentId, getPublishedId} from '@sanity/id-utils'
|
|
2
2
|
import {type SanityProjectionResult} from 'groq'
|
|
3
|
-
import {omit} from 'lodash-es'
|
|
4
3
|
|
|
5
4
|
import {type DocumentHandle} from '../config/sanityConfig'
|
|
6
|
-
import {
|
|
5
|
+
import {bindActionByResourceAndPerspective} from '../store/createActionBinder'
|
|
7
6
|
import {type SanityInstance} from '../store/createSanityInstance'
|
|
8
7
|
import {
|
|
9
8
|
createStateSourceAction,
|
|
@@ -12,6 +11,7 @@ import {
|
|
|
12
11
|
} from '../store/createStateSourceAction'
|
|
13
12
|
import {hashString} from '../utils/hashString'
|
|
14
13
|
import {insecureRandomId} from '../utils/ids'
|
|
14
|
+
import {omitProperty} from '../utils/object'
|
|
15
15
|
import {setCleanupTimeout} from '../utils/setCleanupTimeout'
|
|
16
16
|
import {projectionStore} from './projectionStore'
|
|
17
17
|
import {type ProjectionStoreState, type ProjectionValuePending} from './types'
|
|
@@ -72,7 +72,7 @@ export function getProjectionState(
|
|
|
72
72
|
/**
|
|
73
73
|
* @beta
|
|
74
74
|
*/
|
|
75
|
-
export const _getProjectionState =
|
|
75
|
+
export const _getProjectionState = bindActionByResourceAndPerspective(
|
|
76
76
|
projectionStore,
|
|
77
77
|
createStateSourceAction({
|
|
78
78
|
selector: (
|
|
@@ -113,7 +113,7 @@ export const _getProjectionState = bindActionBySourceAndPerspective(
|
|
|
113
113
|
return () => {
|
|
114
114
|
setCleanupTimeout(() => {
|
|
115
115
|
state.set('removeSubscription', (prev): Partial<ProjectionStoreState> => {
|
|
116
|
-
const documentSubscriptionsForHash =
|
|
116
|
+
const documentSubscriptionsForHash = omitProperty(
|
|
117
117
|
prev.subscriptions[documentId]?.[projectionHash],
|
|
118
118
|
subscriptionId,
|
|
119
119
|
)
|
|
@@ -30,7 +30,7 @@ describe('projectionStore', () => {
|
|
|
30
30
|
instance,
|
|
31
31
|
{
|
|
32
32
|
name: 'p.d',
|
|
33
|
-
|
|
33
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
34
34
|
perspective: 'drafts',
|
|
35
35
|
},
|
|
36
36
|
projectionStore,
|
|
@@ -42,7 +42,7 @@ describe('projectionStore', () => {
|
|
|
42
42
|
state,
|
|
43
43
|
key: {
|
|
44
44
|
name: 'p.d',
|
|
45
|
-
|
|
45
|
+
resource: {projectId: 'p', dataset: 'd'},
|
|
46
46
|
perspective: 'drafts',
|
|
47
47
|
},
|
|
48
48
|
})
|