Repository: sanity-io/sanity
Pull request #9386: feat: use sanity client to resolve GDR references
Author: login: sgulseth
Created: 2025-05-09T13:05:01Z | Merged: 2025-05-12T09:46:46Z

### Description

We were using a "fake" resource client to resolve GDR referenced documents. Now that we have resource support in the sanity client we can use the client to resolve these.

### What to review

Correctness

### Testing

Since this is more of a refactor, the functionality should stay the same, so no tests should be added or removed.

### Notes for release

N/A : No notes needed, more an internal improvements for future maintainability 


Diff:
diff --git a/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/StudioGlobalDocumentReferenceInput.tsx b/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/StudioGlobalDocumentReferenceInput.tsx
index c6c9471a1534..180a0e91ae9c 100644
--- a/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/StudioGlobalDocumentReferenceInput.tsx
+++ b/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/StudioGlobalDocumentReferenceInput.tsx
@@ -81,7 +81,7 @@ export function StudioGlobalDocumentReferenceInput(
   const {path, schemaType} = props
   const source = useSource()
   const client = source.getClient({
-    apiVersion: '2023-11-13',
+    apiVersion: '2025-02-19',
   })
   const getClient = source.getClient
   const {strategy: searchStrategy} = source.search
diff --git a/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceClient.ts b/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceClient.ts
index 4de048c060c8..c6b8f04c5db6 100644
--- a/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceClient.ts
+++ b/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceClient.ts
@@ -1,212 +1,28 @@
-import {
-  type Any,
-  type ClientReturn,
-  connectEventSource,
-  type QueryWithoutParams,
-  type SanityClient,
-  type SanityDocument,
-  type ServerSentEvent,
-} from '@sanity/client'
+import {type SanityClient} from '@sanity/client'
 import {type GlobalDocumentReferenceSchemaType} from '@sanity/types'
-import {map, type Observable} from 'rxjs'
-
-import {
-  globalDatasetApiVersion,
-  globalDocumentReferenceApiVersion as apiVersion,
-} from '../constants'
-
-export type ReferenceClient = {
-  getDocument<R extends Record<string, Any>>(
-    id: string,
-    searchParams?: URLSearchParams,
-  ): Observable<SanityDocument<R> | null>
-  getDocuments<R extends Record<string, Any>>(
-    ids: string[],
-    searchParams?: URLSearchParams,
-  ): Observable<{
-    documents: SanityDocument<R>[]
-    omitted: {id: string; reason: 'existence' | 'permission'}[]
-  }>
-  query<
-    R = Any,
-    Q extends Record<string, unknown> | undefined = QueryWithoutParams,
-    G extends string = string,
-  >(
-    query: G,
-    params?: Q | QueryWithoutParams,
-  ): Observable<ClientReturn<G, R>>
-  listen<
-    Q extends Record<string, string> | undefined = QueryWithoutParams,
-    G extends string = string,
-  >(
-    query: G,
-    params?: Q | QueryWithoutParams,
-    opts?: {includeResult?: boolean},
-  ): Observable<ServerSentEvent<'mutation' | 'welcome'>>
-}
 
 export function getReferenceClient(
   client: SanityClient,
   schemaType: GlobalDocumentReferenceSchemaType,
-): ReferenceClient {
+): SanityClient {
   if (schemaType.resourceType === 'dataset') {
     const [projectId, datasetName] = schemaType.resourceId.split('.', 2)
-    return {
-      getDocument<R extends Record<string, Any>>(
-        id: string,
-        searchParams?: URLSearchParams,
-      ): Observable<SanityDocument<R> | null> {
-        const tag = searchParams?.get('tag') || undefined
-        searchParams?.delete('tag')
-        return client
-          .withConfig({
-            useProjectHostname: false,
-            apiVersion: globalDatasetApiVersion,
-          })
-          .observable.request({
-            useGlobalApi: true,
-            uri: `/projects/${projectId}/datasets/${datasetName}/doc/${id}?${searchParams?.toString() || ''}`,
-            method: 'GET',
-            tag,
-          })
-          .pipe(map((res) => res.documents[0]))
-      },
-      getDocuments<R extends Record<string, Any>>(
-        ids: string[],
-        searchParams?: URLSearchParams,
-      ): Observable<{
-        documents: SanityDocument<R>[]
-        omitted: {id: string; reason: 'existence' | 'permission'}[]
-      }> {
-        const tag = searchParams?.get('tag') || undefined
-        searchParams?.delete('tag')
-        return client
-          .withConfig({
-            useProjectHostname: false,
-            apiVersion: globalDatasetApiVersion,
-          })
-          .observable.request({
-            useGlobalApi: true,
-            uri: `/projects/${projectId}/datasets/${datasetName}/doc/${ids.join(',')}?${searchParams?.toString() || ''}`,
-            method: 'GET',
-            tag,
-          })
-      },
-      query<
-        R = Any,
-        Q extends Record<string, unknown> | undefined = QueryWithoutParams,
-        G extends string = string,
-      >(query: G, params: Q) {
-        return client.observable
-          .withConfig({
-            useProjectHostname: false,
-            apiVersion: globalDatasetApiVersion,
-          })
-          .request<{result: ClientReturn<G, R>}>({
-            url: `/projects/${projectId}/datasets/${datasetName}/query`,
-            useGlobalApi: true,
-            method: 'POST',
-            body: {query, params},
-            tag: 'sanity.studio.gdr.query',
-          })
-          .pipe(map((res) => res.result))
+    return client.withConfig({
+      'apiVersion': 'X',
+      '~experimental_resource': {
+        type: 'dataset',
+        id: `${projectId}.${datasetName}`,
       },
-      listen<
-        Q extends Record<string, string> | undefined = QueryWithoutParams,
-        G extends string = string,
-      >(query: G, params: Q, opts: {includeResult?: boolean} = {}) {
-        // add $ as a prefix to all keys in the params
-        const queryParams = Object.keys(params || {}).reduce<Record<string, string>>((acc, key) => {
-          acc[`$${key}`] = `"${params![key]}"`
-          return acc
-        }, {})
-        const allParams = {...queryParams, tag: 'sanity.studio.listen.gdr', query} as Record<
-          string,
-          string
-        >
-        if (opts.includeResult) {
-          allParams.includeResult = 'true'
-        }
-        const paramsString = new URLSearchParams(allParams).toString()
-        const uri = `${client.config().apiHost}/${globalDatasetApiVersion}/projects/${projectId}/datasets/${datasetName}/listen?${paramsString}`
-        return connectEventSource(
-          () => new EventSource(uri, {withCredentials: true}),
-          ['welcome', 'mutation'],
-        )
-      },
-    } satisfies ReferenceClient
+    })
   }
-  if (schemaType.resourceType === 'media-library') {
-    return {
-      getDocument<R extends Record<string, Any>>(
-        id: string,
-        searchParams?: URLSearchParams,
-      ): Observable<SanityDocument<R> | null> {
-        const tag = searchParams?.get('tag') || undefined
-        searchParams?.delete('tag')
-        return client
-          .withConfig({
-            useProjectHostname: false,
-            apiVersion,
-          })
-          .observable.request({
-            useGlobalApi: true,
-            uri: `/media-libraries/${schemaType.resourceId}/doc/${id}?${searchParams?.toString() || ''}`,
-            method: 'GET',
-            tag,
-          })
-          .pipe(map((res) => res.documents[0]))
-      },
-      getDocuments<R extends Record<string, Any>>(
-        ids: string[],
-        searchParams?: URLSearchParams,
-      ): Observable<{
-        documents: SanityDocument<R>[]
-        omitted: {id: string; reason: 'existence' | 'permission'}[]
-      }> {
-        const tag = searchParams?.get('tag') || undefined
-        searchParams?.delete('tag')
-        return client
-          .withConfig({
-            useProjectHostname: false,
-            apiVersion,
-          })
-          .observable.request({
-            useGlobalApi: true,
-            uri: `/${apiVersion}/media-libraries/${schemaType.resourceId}/doc/${ids.join(',')}?${searchParams?.toString() || ''}`,
-            method: 'GET',
-            tag,
-          })
-      },
-      query<
-        R = Any,
-        Q extends Record<string, unknown> | undefined = QueryWithoutParams,
-        G extends string = string,
-      >(query: G, params: Q) {
-        return client
-          .withConfig({
-            useProjectHostname: false,
-            apiVersion,
-          })
-          .observable.request<{result: ClientReturn<G, R>}>({
-            useGlobalApi: true,
-            uri: `/media-libraries/${schemaType.resourceId}/query`,
-            method: 'POST',
-            body: {query, params},
-            tag: 'gdr.query',
-          })
-          .pipe(map((res) => res.result))
-      },
-      listen<
-        Q extends Record<string, string> | undefined = QueryWithoutParams,
-        G extends string = string,
-      >(query: G, params: Q) {
-        const allParams = {...params, tag: 'listen', query}
-        const paramsString = new URLSearchParams(allParams).toString()
-        const uri = `${client.config().apiHost}/${apiVersion}/media-libraries/${schemaType.resourceId}/listen?${paramsString}`
-        return connectEventSource(() => new EventSource(uri, {}), ['welcome', 'mutation'])
-      },
-    } satisfies ReferenceClient
+  if (schemaType.resourceType === 'media-library' || schemaType.resourceType === 'canvas') {
+    return client.withConfig({
+      'apiVersion': '2025-02-19',
+      '~experimental_resource': {
+        type: schemaType.resourceType,
+        id: schemaType.resourceId,
+      },
+    })
   }
   throw new Error(`Invalid resource type "${schemaType.resourceType}"`)
 }
diff --git a/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts b/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts
index 264280f5e9a0..ee1118751cf5 100644
--- a/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts
+++ b/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts
@@ -1,18 +1,13 @@
-import {type GlobalDocumentReferenceSchemaType, type SanityDocument} from '@sanity/types'
+import {type ListenEvent, type SanityClient, type SanityDocument} from '@sanity/client'
+import {type GlobalDocumentReferenceSchemaType} from '@sanity/types'
 import {keyBy} from 'lodash'
-import {combineLatest, EMPTY, type Observable, of, share} from 'rxjs'
+import {combineLatest, EMPTY, type Observable, of} from 'rxjs'
 import {map, switchMap} from 'rxjs/operators'
 
-import {
-  type DocumentAvailability,
-  type FieldName,
-  getPreviewPaths,
-  prepareForPreview,
-} from '../../../../../preview'
+import {type DocumentAvailability, getPreviewPaths, prepareForPreview} from '../../../../../preview'
 import {createPathObserver} from '../../../../../preview/createPathObserver'
 import {isRecord} from '../../../../../util'
 import {type GlobalDocumentReferenceInfo} from '../../../../inputs/GlobalDocumentReferenceInput/types'
-import {type ReferenceClient} from './getReferenceClient'
 
 const REQUEST_TAG_BASE = 'gdr'
 
@@ -34,7 +29,7 @@ const AVAILABILITY_NOT_FOUND = {
 /**
  * Takes a client instance and returns a function that can be called to retrieve reference information
  */
-export function createGetReferenceInfo(context: {client: ReferenceClient}) {
+export function createGetReferenceInfo(context: {client: SanityClient}) {
   const {client} = context
 
   /**
@@ -48,7 +43,7 @@ export function createGetReferenceInfo(context: {client: ReferenceClient}) {
     return (
       doc._type
         ? of(doc)
-        : client
+        : client.observable
             .getDocument<{_id: string; _type: string}>(doc._id)
             .pipe(map((res): {_id: string; _type?: string} => ({_id: doc._id, _type: res?._type})))
     ).pipe(
@@ -78,12 +73,26 @@ export function createGetReferenceInfo(context: {client: ReferenceClient}) {
         }
 
         const previewPaths = getPreviewPaths(refSchemaType?.preview) || []
-        const listener = client.listen('*', {}, {includeResult: true}).pipe(share())
+        const listener = client.observable.listen(
+          '*',
+          {},
+          {
+            includeResult: true,
+            tag: `${REQUEST_TAG_BASE}.preview`,
+            // by default listen only emits the "mutation" event, however we use the initial "welcome" event to trigger the initial state
+            events: ['mutation', 'welcome'],
+          },
+        )
         const observeFields = createObserveFields({client, listener})
         const observePaths = createPathObserver({observeFields})
 
         const publishedPreview$ = observePaths(doc, previewPaths).pipe(
-          map((result) => (result ? prepareForPreview(result, refSchemaType) : result)),
+          map((result) => {
+            if (!result) {
+              return null
+            }
+            return prepareForPreview(result, refSchemaType)
+          }),
         )
 
         return combineLatest([publishedPreview$]).pipe(
@@ -104,45 +113,63 @@ export function createGetReferenceInfo(context: {client: ReferenceClient}) {
 }
 
 function createObserveFields(context: {
-  client: ReferenceClient
-  listener: Observable<{type: 'welcome'} | {type: 'mutation'; data?: unknown}>
-}): (id: string, fields: FieldName[]) => Observable<Record<string, unknown> | null> {
+  client: SanityClient
+  listener: Observable<ListenEvent<Record<string, any>>>
+}) {
   const {client, listener} = context
-  return function observeFields(id: string, fields: string[]) {
+  return function observeFields(
+    id: string,
+    fields: string[],
+  ): Observable<SanityDocument<Record<string, any>> | null> {
     return listener.pipe(
       switchMap((event) => {
         if (event.type === 'welcome') {
-          return client.getDocument(id)
+          return client.observable
+            .getDocument(id, {
+              tag: `${REQUEST_TAG_BASE}.get-document`,
+            })
+            .pipe(
+              map((doc) => {
+                if (!doc) {
+                  return null
+                }
+                return doc
+              }),
+            )
         }
-        if (!event.data) {
+        if (event.type !== 'mutation') {
+          return EMPTY
+        }
+        const result = event.result
+        if (!result) {
           return EMPTY
         }
 
-        const data = event.data as {result: SanityDocument; documentId: string; transition: string}
-
-        if (data.documentId !== id) {
+        if (result.documentId !== id) {
           return EMPTY
         }
-        if (data.transition === 'disappear') {
+        if (result.transition === 'disappear') {
           return of(null)
         }
 
-        return of(data.result)
+        return of(result)
       }),
     )
   }
 }
 
 function fetchDocumentAvailability(
-  client: ReferenceClient,
+  client: SanityClient,
   id: string,
 ): Observable<DocumentAvailability | null> {
-  const queryParams = new URLSearchParams({
+  const requestOptions = {
+    uri: client.getDataUrl('doc', id),
+    json: true,
     excludeContent: 'true',
     tag: `${REQUEST_TAG_BASE}.availability`,
-  })
+  }
 
-  return client.getDocuments([id], queryParams).pipe(
+  return client.observable.request(requestOptions).pipe(
     map((response) => {
       const omitted = keyBy(response.omitted || [], (entry) => entry.id)
       const omittedEntry = omitted[id]
diff --git a/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/search.ts b/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/search.ts
index a9f721047d55..4694d81764c7 100644
--- a/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/search.ts
+++ b/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/search.ts
@@ -1,3 +1,4 @@
+import {type SanityClient} from '@sanity/client'
 import {
   type GlobalDocumentReferenceSchemaType,
   type GlobalDocumentReferenceType,
@@ -12,7 +13,6 @@ import {createSearchQuery} from '../../../../../search/groq2024/createSearchQuer
 import {getNextCursor} from '../../../../../search/groq2024/getNextCursor'
 import {type SearchParams} from '../../../../../search/weighted/createSearchQuery'
 import {collate} from '../../../../../util'
-import {type ReferenceClient} from './getReferenceClient'
 
 interface SearchHit {
   id: string
@@ -23,7 +23,7 @@ interface SearchHit {
 const limit = 10
 
 function doSearch(
-  client: ReferenceClient,
+  client: SanityClient,
   searchTerm: string,
   types: GlobalDocumentReferenceType[],
   searchOptions: ReferenceFilterSearchOptions,
@@ -34,7 +34,7 @@ function doSearch(
     searchOptions,
   )
 
-  return client.query<SanityDocumentLike[], SearchParams>(query, params).pipe(
+  return client.observable.fetch<SanityDocumentLike[], SearchParams>(query, params).pipe(
     map((hits) => {
       const hasNextPage = typeof limit !== 'undefined' && hits.length > limit
 
@@ -54,7 +54,7 @@ function doSearch(
 }
 
 export function search(
-  client: ReferenceClient,
+  client: SanityClient,
   textTerm: string,
   type: GlobalDocumentReferenceSchemaType,
   options: ReferenceFilterSearchOptions,


Reviews:
user: login: bjoerge
body: Great to see `hacks--`!

Would appreciate a quick comment clarifying event subscription (see comment below)
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-12T09:22:22Z
user: login: sgulseth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T09:24:46Z
user: login: bjoerge
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-12T09:29:03Z

Review comments:
diff_hunk: @@ -78,12 +73,25 @@ export function createGetReferenceInfo(context: {client: ReferenceClient}) {
         }
 
         const previewPaths = getPreviewPaths(refSchemaType?.preview) || []
-        const listener = client.listen('*', {}, {includeResult: true}).pipe(share())
+        const listener = client.observable.listen(
+          '*',
+          {},
+          {
+            includeResult: true,
+            tag: `${REQUEST_TAG_BASE}.preview`,
+            events: ['mutation', 'welcome'],
path: packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts
user: login: bjoerge
body: non-obvious to me why we don't care about other events. add a code comment explaning why?
created_at: 2025-05-12T09:18:50Z
author_association: MEMBER
diff_hunk: @@ -78,12 +73,25 @@ export function createGetReferenceInfo(context: {client: ReferenceClient}) {
         }
 
         const previewPaths = getPreviewPaths(refSchemaType?.preview) || []
-        const listener = client.listen('*', {}, {includeResult: true}).pipe(share())
+        const listener = client.observable.listen(
+          '*',
+          {},
+          {
+            includeResult: true,
+            tag: `${REQUEST_TAG_BASE}.preview`,
+            events: ['mutation', 'welcome'],
path: packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts
user: login: sgulseth
body: yesss 👍 
created_at: 2025-05-12T09:24:46Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-05-09T13:05:07Z
body: [vc]: #ziEZSItMhlN29LoKUvuiaIcjZAl4U5fYwaHOvDoLufs=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0ZXN0LXN0dWRpbyIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zYW5pdHktc2FuZGJveC90ZXN0LXN0dWRpby83TmlGU1BDdEFFeUR4eGYxRGtpUUszRTFjbXV0IiwicHJldmlld1VybCI6InRlc3Qtc3R1ZGlvLWdpdC1mZWF0LWdkcnNhbml0eS1jbGllbnQuc2FuaXR5LmRldiIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ0ZXN0LXN0dWRpby1naXQtZmVhdC1nZHJzYW5pdHktY2xpZW50LnNhbml0eS5kZXYifX0seyJuYW1lIjoic3R1ZGlvLXdvcmtzaG9wIiwicm9vdERpcmVjdG9yeSI6bnVsbCwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvc3R1ZGlvLXdvcmtzaG9wLzljZmdiNnlOdHhuVGFtMkxpR3EyejU0YUxmaTciLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsInByZXZpZXdVcmwiOiJzdHVkaW8td29ya3Nob3AtZ2l0LWZlYXQtZ2Ryc2FuaXR5LWNsaWVudC5zYW5pdHkuZGV2In0seyJuYW1lIjoicGVyZm9ybWFuY2Utc3R1ZGlvIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Nhbml0eS1zYW5kYm94L3BlcmZvcm1hbmNlLXN0dWRpby9CTnpZdEhuYVRlc1RuVkVYazJDaUZRRnhQTm1NIiwicHJldmlld1VybCI6InBlcmZvcm1hbmNlLXN0dWRpby1naXQtZmVhdC1nZHJzYW5pdHktY2xpZW50LnNhbml0eS5kZXYiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicGVyZm9ybWFuY2Utc3R1ZGlvLWdpdC1mZWF0LWdkcnNhbml0eS1jbGllbnQuc2FuaXR5LmRldiJ9fSx7Im5hbWUiOiJ0ZXN0LW5leHQtc3R1ZGlvIiwicm9vdERpcmVjdG9yeSI6ImRldi90ZXN0LW5leHQtc3R1ZGlvIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvdGVzdC1uZXh0LXN0dWRpby9CbTN6dVFlNEVIbmpIeWs1aXg5cm4xODc5cEI0IiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQifSx7Im5hbWUiOiJwYWdlLWJ1aWxkaW5nLXN0dWRpbyIsInJvb3REaXJlY3RvcnkiOiJkZXYvcGFnZS1idWlsZGluZy1zdHVkaW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvcGFnZS1idWlsZGluZy1zdHVkaW8vR21YcWJwOW0yV25nNm9aSmMzcFNFZ0pmZXVoUCIsInByZXZpZXdVcmwiOiJwYWdlLWJ1aWxkaW5nLXN0dWRpby1naXQtZmVhdC1nZHJzYW5pdHktY2xpZW50LnNhbml0eS5kZXYiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicGFnZS1idWlsZGluZy1zdHVkaW8tZ2l0LWZlYXQtZ2Ryc2FuaXR5LWNsaWVudC5zYW5pdHkuZGV2In19LHsibmFtZSI6ImUyZS1zdHVkaW8iLCJyb290RGlyZWN0b3J5IjpudWxsLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zYW5pdHktc2FuZGJveC9lMmUtc3R1ZGlvL0NHU1o1ajUyNjhkcEM2S2dXYnRDMzN0MzVOOFkiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **page-building-studio** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/page-building-studio/GmXqbp9m2Wng6oZJc3pSEgJfeuhP)) | [Visit Preview](https://page-building-studio-git-feat-gdrsanity-client.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/page-building-studio-git-feat-gdrsanity-client.sanity.dev?via=pr-comment-feedback-link) | May 12, 2025 9:31am |
| **performance-studio** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/performance-studio/BNzYtHnaTesTnVEXk2CiFQFxPNmM)) | [Visit Preview](https://performance-studio-git-feat-gdrsanity-client.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/performance-studio-git-feat-gdrsanity-client.sanity.dev?via=pr-comment-feedback-link) | May 12, 2025 9:31am |
| **test-studio** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/test-studio/7NiFSPCtAEyDxxf1DkiQK3E1cmut)) | [Visit Preview](https://test-studio-git-feat-gdrsanity-client.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/test-studio-git-feat-gdrsanity-client.sanity.dev?via=pr-comment-feedback-link) | May 12, 2025 9:31am |

<details><summary>3 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **e2e-studio** | ⬜️ Ignored ([Inspect](https://vercel.com/sanity-sandbox/e2e-studio/CGSZ5j5268dpC6KgWbtC33t35N8Y)) |  |  | May 12, 2025 9:31am |
| **studio-workshop** | ⬜️ Ignored ([Inspect](https://vercel.com/sanity-sandbox/studio-workshop/9cfgb6yNtxnTam2LiGq2z54aLfi7)) | [Visit Preview](https://studio-workshop-git-feat-gdrsanity-client.sanity.dev) |  | May 12, 2025 9:31am |
| **test-next-studio** | ⬜️ Ignored ([Inspect](https://vercel.com/sanity-sandbox/test-next-studio/Bm3zuQe4EHnjHyk5ix9rn1879pB4)) |  |  | May 12, 2025 9:31am |
</details>







author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-09T13:08:44Z
body: No changes to documentation
<!-- thollander/actions-comment-pull-request "docs-report" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-09T13:10:25Z
body: <h2>Coverage Report</h2>
<table> <thead> <tr> <th align="center">Status</th> <th align="left">Category</th> <th align="right">Percentage</th> <th align="right">Covered / Total</th> </tr> </thead> <tbody> <tr> <td align="center">🔵</td> <td align="left">Lines</td> <td align="right">43.18%</td> <td align="right">57666 / 133528</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Statements</td> <td align="right">43.18%</td> <td align="right">57666 / 133528</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Functions</td> <td align="right">47.45%</td> <td align="right">2872 / 6052</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Branches</td> <td align="right">79.66%</td> <td align="right">10702 / 13433</td> </tr> </tbody> </table><details><summary>File Coverage</summary><table> <thead> <tr> <th align="left">File</th> <th align="right">Stmts</th> <th align="right">Branches</th> <th align="right">Functions</th> <th align="right">Lines</th> <th align="left">Uncovered Lines</th> </tr> </thead> <tbody> <tr> <td align="left" colspan="6"><b>Changed Files</b></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/StudioGlobalDocumentReferenceInput.tsx">packages/sanity/src/core/form/studio/inputs/globalDocumentReference/StudioGlobalDocumentReferenceInput.tsx</a></td> <td align="right">13.09%</td> <td align="right">100%</td> <td align="right">0%</td> <td align="right">13.09%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/StudioGlobalDocumentReferenceInput.tsx#L24-L62" class="text-red">24-62</a>, <a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/StudioGlobalDocumentReferenceInput.tsx#L79-L133" class="text-red">79-133</a></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceClient.ts">packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceClient.ts</a></td> <td align="right">4%</td> <td align="right">100%</td> <td align="right">0%</td> <td align="right">4%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceClient.ts#L5-L28" class="text-red">5-28</a></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts">packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts</a></td> <td align="right">12.65%</td> <td align="right">100%</td> <td align="right">0%</td> <td align="right">12.65%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/getReferenceInfo.ts#L33-L191" class="text-red">33-191</a></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/search.ts">packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/search.ts</a></td> <td align="right">13.04%</td> <td align="right">100%</td> <td align="right">0%</td> <td align="right">13.04%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/search.ts#L25-L54" class="text-red">25-54</a>, <a href="https://github.com/sanity-io/sanity/blob/067c71a608367cab0dda992d503a3d5dd2c4537c/packages/sanity/src/core/form/studio/inputs/globalDocumentReference/datastores/search.ts#L57-L73" class="text-red">57-73</a></td> </tr> </tbody> </table></details>
<em>Generated in workflow <a href=https://github.com/sanity-io/sanity/actions/runs/14968642843>#35031</a> for commit <a href="https://github.com/sanity-io/sanity/commit/067c71a608367cab0dda992d503a3d5dd2c4537c">067c71a</a> by the <a href="https://github.com/davelosert/vitest-coverage-report-action">Vitest Coverage Report Action</a></em>

<!-- vitest-coverage-report-marker-root -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-09T13:13:16Z
body: ### 🧪 [E2E Preview environment](https://e2e-studio-d2snma2v4.sanity.dev) 

<details>
<summary> 🔑 Environment Variables for Local Testing</summary>

This is the preview URL for the E2E tests: **https://e2e-studio-d2snma2v4.sanity.dev**

To run the E2E tests locally, you can use the following environment variables, then run `pnpm test:e2e --ui` to open the Playwright test runner.

💬 Remember to build the project first with `pnpm build:e2e`.

```
  SANITY_E2E_PROJECT_ID=ittbm412
  SANITY_E2E_BASE_URL=https://e2e-studio-d2snma2v4.sanity.dev
  SANITY_E2E_DATASET="update depending the project you want to test (pr-9386-chromium-14968642829 || pr-9386-firefox-14968642829 )"
  SANITY_E2E_DATASET_CHROMIUM=pr-9386-chromium-14968642829
  SANITY_E2E_DATASET_FIREFOX=pr-9386-firefox-14968642829
```

</details>
<!-- thollander/actions-comment-pull-request "e2e-preview-url" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-09T13:16:13Z
body: ### ⚡️ Editor Performance Report

Updated Mon, 12 May 2025 09:40:34 GMT


| Benchmark | reference<br/><sup>latency of `sanity@latest`</sup> | experiment<br/><sup>latency of this branch</sup> | Δ (%)<br/><sup>latency difference</sup> | |
| :-- | :-- | :-- | :-- | --- |
| article (title) | 27.0 efps (37ms) | 26.3 efps (38ms) | +1ms (+2.7%) | ✅ |
| article (body) | 44.0 efps (23ms) | 53.1 efps (19ms) | -4ms (-17.1%) | ✅ |
| article (string inside object) | 27.8 efps (36ms) | 28.6 efps (35ms) | -1ms (-2.8%) | ✅ |
| article (string inside array) | 23.3 efps (43ms) | 24.4 efps (41ms) | -2ms (-4.7%) | ✅ |
| recipe (name) | 45.5 efps (22ms) | 45.5 efps (22ms) | +0ms (-/-%) | ✅ |
| recipe (description) | 50.0 efps (20ms) | 50.0 efps (20ms) | +0ms (-/-%) | ✅ |
| recipe (instructions) | 99.9+ efps (6ms) | 99.9+ efps (6ms) | -0ms (-/-%) | ✅ |
| synthetic (title) | 19.6 efps (51ms) | 18.7 efps (54ms) | +3ms (+4.9%) | ✅ |
| synthetic (string inside object) | 17.2 efps (58ms) | 18.9 efps (53ms) | -5ms (-8.6%) | ✅ |


> **efps** — editor "frames per second". The number of updates assumed to be possible within a second.
>
> Derived from input latency. `efps = 1000 / input_latency`

<details>

<summary><strong>Detailed information</strong></summary>

### 🏠 Reference result

The performance result of `sanity@latest`



| Benchmark | latency | p75 | p90 | p99 | blocking time | test duration |
| --------- | ------: | --: | --: | --: | ------------: | ------------: |
| article (title) | 37ms | 40ms | 43ms | 56ms | 38ms | 10.1s |
| article (body) | 23ms | 27ms | 47ms | 221ms | 298ms | 6.6s |
| article (string inside object) | 36ms | 38ms | 41ms | 59ms | 3ms | 6.7s |
| article (string inside array) | 43ms | 45ms | 52ms | 173ms | 174ms | 7.3s |
| recipe (name) | 22ms | 24ms | 26ms | 39ms | 0ms | 8.3s |
| recipe (description) | 20ms | 21ms | 23ms | 52ms | 0ms | 5.0s |
| recipe (instructions) | 6ms | 8ms | 9ms | 11ms | 0ms | 3.2s |
| synthetic (title) | 51ms | 54ms | 59ms | 70ms | 613ms | 12.3s |
| synthetic (string inside object) | 58ms | 60ms | 76ms | 105ms | 1065ms | 8.7s |


### 🧪 Experiment result

The performance result of this branch


| Benchmark | latency | p75 | p90 | p99 | blocking time | test duration |
| --------- | ------: | --: | --: | --: | ------------: | ------------: |
| article (title) | 38ms | 40ms | 46ms | 145ms | 168ms | 10.3s |
| article (body) | 19ms | 21ms | 24ms | 65ms | 46ms | 5.2s |
| article (string inside object) | 35ms | 37ms | 39ms | 126ms | 126ms | 6.4s |
| article (string inside array) | 41ms | 43ms | 45ms | 55ms | 173ms | 6.9s |
| recipe (name) | 22ms | 22ms | 24ms | 50ms | 8ms | 8.6s |
| recipe (description) | 20ms | 21ms | 24ms | 53ms | 0ms | 5.2s |
| recipe (instructions) | 6ms | 8ms | 9ms | 15ms | 0ms | 3.3s |
| synthetic (title) | 54ms | 57ms | 60ms | 140ms | 612ms | 12.1s |
| synthetic (string inside object) | 53ms | 55ms | 57ms | 348ms | 1064ms | 8.2s |


### 📚 Glossary

> #### column definitions
>
> - **benchmark** — the name of the test, e.g. "article", followed by the label of the field being measured, e.g. "(title)".
> - **latency** — the time between when a key was pressed and when it was rendered. derived from a set of samples. the median (p50) is shown to show the most common latency.
> - **p75** — the 75th percentile of the input latency in the test run. 75% of the sampled inputs in this benchmark were processed faster than this value. this provides insight into the upper range of typical performance.
> - **p90** — the 90th percentile of the input latency in the test run. 90% of the sampled inputs were faster than this. this metric helps identify slower interactions that occurred less frequently during the benchmark.
> - **p99** — the 99th percentile of the input latency in the test run. only 1% of sampled inputs were slower than this. this represents the worst-case scenarios encountered during the benchmark, useful for identifying potential performance outliers.
> - **blocking time** — the total time during which the main thread was blocked, preventing user input and UI updates. this metric helps identify performance bottlenecks that may cause the interface to feel unresponsive.
> - **test duration** — how long the test run took to complete.

</details>

<!-- thollander/actions-comment-pull-request "efps-report" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-09T13:18:54Z
body: ### 📊 Playwright Test Report

**[Download Full E2E Report](https://github.com/sanity-io/sanity/actions/runs/14968642829/artifacts/3104861102)**

This report contains test results, including videos of failing tests.
<!-- thollander/actions-comment-pull-request "playwright-e2e-report" -->
author_association: CONTRIBUTOR