@sanity/sdk-react 0.0.0-alpha.26 → 0.0.0-alpha.28

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/index.d.ts CHANGED
@@ -96,6 +96,18 @@ declare interface AuthBoundaryProps {
96
96
  */
97
97
  export declare type ComlinkStatus = 'idle' | 'handshaking' | 'connected' | 'disconnected'
98
98
 
99
+ declare interface DashboardResource {
100
+ id: string
101
+ name: string
102
+ title: string
103
+ basePath: string
104
+ projectId: string
105
+ dataset: string
106
+ type: string
107
+ userApplicationId: string
108
+ url: string
109
+ }
110
+
99
111
  /** @public */
100
112
  declare type DatasetAclMode = 'public' | 'private' | 'custom'
101
113
 
@@ -1271,6 +1283,8 @@ declare interface UseManageFavoriteProps extends DocumentHandle {
1271
1283
  *
1272
1284
  * @category Documents
1273
1285
  * @param documentHandle - The document handle for the document to navigate to
1286
+ * @param preferredStudioUrl - The preferred studio url to navigate to if you have multiple
1287
+ * studios with the same projectId and dataset
1274
1288
  * @returns An object containing:
1275
1289
  * - `navigateToStudioDocument` - Function that when called will navigate to the studio document
1276
1290
  * - `isConnected` - Boolean indicating if connection to Dashboard is established
@@ -1292,6 +1306,7 @@ declare interface UseManageFavoriteProps extends DocumentHandle {
1292
1306
  */
1293
1307
  export declare function useNavigateToStudioDocument(
1294
1308
  documentHandle: DocumentHandle,
1309
+ preferredStudioUrl?: string,
1295
1310
  ): NavigateToStudioResult
1296
1311
 
1297
1312
  /**
@@ -1854,18 +1869,8 @@ export declare type WindowMessageHandler<TFrameMessage extends FrameMessage> = (
1854
1869
  event: TFrameMessage['data'],
1855
1870
  ) => TFrameMessage['response']
1856
1871
 
1857
- declare interface Workspace {
1858
- name: string
1859
- title: string
1860
- basePath: string
1861
- dataset: string
1862
- userApplicationId: string
1863
- url: string
1864
- _ref: string
1865
- }
1866
-
1867
1872
  declare interface WorkspacesByProjectIdDataset {
1868
- [key: `${string}:${string}`]: Workspace[]
1873
+ [key: `${string}:${string}`]: DashboardResource[]
1869
1874
  }
1870
1875
 
1871
1876
  export * from '@sanity/sdk'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/sdk-react",
3
- "version": "0.0.0-alpha.26",
3
+ "version": "0.0.0-alpha.28",
4
4
  "private": false,
5
5
  "description": "Sanity SDK React toolkit for Content OS",
6
6
  "keywords": [
@@ -47,9 +47,10 @@
47
47
  "@sanity/types": "^3.83.0",
48
48
  "@types/lodash-es": "^4.17.12",
49
49
  "lodash-es": "^4.17.21",
50
+ "react-compiler-runtime": "19.0.0-beta-ebf51a3-20250411",
50
51
  "react-error-boundary": "^5.0.0",
51
52
  "rxjs": "^7.8.2",
52
- "@sanity/sdk": "0.0.0-alpha.26"
53
+ "@sanity/sdk": "0.0.0-alpha.28"
53
54
  },
54
55
  "devDependencies": {
55
56
  "@sanity/browserslist-config": "^1.0.5",
@@ -63,15 +64,15 @@
63
64
  "@types/react-dom": "^19.1.1",
64
65
  "@vitejs/plugin-react": "^4.3.4",
65
66
  "@vitest/coverage-v8": "3.1.1",
66
- "babel-plugin-react-compiler": "19.0.0-beta-e993439-20250328",
67
+ "babel-plugin-react-compiler": "19.0.0-beta-ebf51a3-20250411",
67
68
  "eslint": "^9.22.0",
68
69
  "jsdom": "^25.0.1",
69
70
  "prettier": "^3.5.3",
70
71
  "react": "^19.1.0",
71
72
  "react-dom": "^19.1.0",
72
73
  "typescript": "^5.7.3",
73
- "vite": "^6.2.5",
74
- "vitest": "^3.1.1",
74
+ "vite": "^6.3.2",
75
+ "vitest": "^3.1.2",
75
76
  "@repo/config-eslint": "0.0.0",
76
77
  "@repo/config-test": "0.0.1",
77
78
  "@repo/package.bundle": "3.82.0",
@@ -1,5 +1,5 @@
1
1
  import {createSanityInstance, type SanityConfig, type SanityInstance} from '@sanity/sdk'
2
- import {Suspense, use, useEffect, useMemo, useRef} from 'react'
2
+ import {Suspense, useContext, useEffect, useMemo, useRef} from 'react'
3
3
 
4
4
  import {SanityInstanceContext} from './SanityInstanceContext'
5
5
 
@@ -72,7 +72,7 @@ export function ResourceProvider({
72
72
  fallback,
73
73
  ...config
74
74
  }: ResourceProviderProps): React.ReactNode {
75
- const parent = use(SanityInstanceContext)
75
+ const parent = useContext(SanityInstanceContext)
76
76
  const instance = useMemo(
77
77
  () => (parent ? parent.createChild(config) : createSanityInstance(config)),
78
78
  [config, parent],
@@ -1,5 +1,5 @@
1
1
  import {type SanityConfig, type SanityInstance} from '@sanity/sdk'
2
- import {use} from 'react'
2
+ import {useContext} from 'react'
3
3
 
4
4
  import {SanityInstanceContext} from '../../context/SanityInstanceContext'
5
5
 
@@ -58,7 +58,7 @@ import {SanityInstanceContext} from '../../context/SanityInstanceContext'
58
58
  * @throws Error if no matching instance is found for the provided config
59
59
  */
60
60
  export const useSanityInstance = (config?: SanityConfig): SanityInstance => {
61
- const instance = use(SanityInstanceContext)
61
+ const instance = useContext(SanityInstanceContext)
62
62
 
63
63
  if (!instance) {
64
64
  throw new Error(
@@ -43,13 +43,13 @@ describe('useNavigateToStudioDocument', () => {
43
43
  }
44
44
 
45
45
  const mockWorkspace = {
46
+ id: 'workspace123',
46
47
  name: 'workspace1',
47
48
  title: 'Workspace 1',
48
49
  basePath: '/workspace1',
49
50
  dataset: 'dataset1',
50
51
  userApplicationId: 'user1',
51
52
  url: 'https://test.sanity.studio',
52
- _ref: 'workspace123',
53
53
  }
54
54
 
55
55
  beforeEach(() => {
@@ -129,7 +129,7 @@ describe('useNavigateToStudioDocument', () => {
129
129
 
130
130
  it('warns when multiple workspaces are found', () => {
131
131
  const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
132
- const mockWorkspace2 = {...mockWorkspace, _ref: 'workspace2'}
132
+ const mockWorkspace2 = {...mockWorkspace, id: 'workspace2'}
133
133
 
134
134
  mockWorkspacesByProjectIdAndDataset = {
135
135
  'project1:dataset1': [mockWorkspace, mockWorkspace2],
@@ -145,16 +145,132 @@ describe('useNavigateToStudioDocument', () => {
145
145
  result.current.navigateToStudioDocument()
146
146
 
147
147
  expect(consoleSpy).toHaveBeenCalledWith(
148
- 'Multiple workspaces found for document',
148
+ 'Multiple workspaces found for document and no preferred studio url',
149
149
  mockDocumentHandle,
150
150
  )
151
151
  expect(mockSendMessage).toHaveBeenCalledWith(
152
152
  'dashboard/v1/bridge/navigate-to-resource',
153
153
  expect.objectContaining({
154
- resourceId: mockWorkspace._ref,
154
+ resourceId: mockWorkspace.id,
155
155
  }),
156
156
  )
157
157
 
158
158
  consoleSpy.mockRestore()
159
159
  })
160
+
161
+ it('warns and does not navigate when projectId or dataset is missing', () => {
162
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
163
+
164
+ const incompleteDocumentHandle: DocumentHandle = {
165
+ documentId: 'doc123',
166
+ documentType: 'article',
167
+ // missing projectId and dataset
168
+ }
169
+
170
+ const {result} = renderHook(() => useNavigateToStudioDocument(incompleteDocumentHandle))
171
+
172
+ // Simulate connection
173
+ act(() => {
174
+ mockStatusCallback?.('connected')
175
+ })
176
+
177
+ result.current.navigateToStudioDocument()
178
+
179
+ expect(consoleSpy).toHaveBeenCalledWith(
180
+ 'Project ID and dataset are required to navigate to a studio document',
181
+ )
182
+ expect(mockSendMessage).not.toHaveBeenCalled()
183
+
184
+ consoleSpy.mockRestore()
185
+ })
186
+
187
+ it('uses preferred studio URL when multiple workspaces are available', () => {
188
+ const preferredUrl = 'https://preferred.sanity.studio'
189
+ const mockWorkspace2 = {...mockWorkspace, id: 'workspace2', url: preferredUrl}
190
+
191
+ mockWorkspacesByProjectIdAndDataset = {
192
+ 'project1:dataset1': [mockWorkspace, mockWorkspace2],
193
+ }
194
+
195
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
196
+
197
+ // Simulate connection
198
+ act(() => {
199
+ mockStatusCallback?.('connected')
200
+ })
201
+
202
+ result.current.navigateToStudioDocument()
203
+
204
+ // Should choose workspace2 because it matches the preferred URL
205
+ expect(mockSendMessage).toHaveBeenCalledWith(
206
+ 'dashboard/v1/bridge/navigate-to-resource',
207
+ expect.objectContaining({
208
+ resourceId: 'workspace2',
209
+ }),
210
+ )
211
+ })
212
+
213
+ it('considers NO_PROJECT_ID:NO_DATASET workspaces when matching preferred URL', () => {
214
+ const preferredUrl = 'https://preferred.sanity.studio'
215
+ // Only have a workspace without projectId/dataset that matches the preferred URL
216
+ const mockWorkspaceNoProject = {
217
+ ...mockWorkspace,
218
+ id: 'workspace3',
219
+ url: preferredUrl,
220
+ projectId: undefined,
221
+ dataset: undefined,
222
+ }
223
+
224
+ mockWorkspacesByProjectIdAndDataset = {
225
+ 'NO_PROJECT_ID:NO_DATASET': [mockWorkspaceNoProject],
226
+ }
227
+
228
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
229
+
230
+ // Simulate connection
231
+ act(() => {
232
+ mockStatusCallback?.('connected')
233
+ })
234
+
235
+ result.current.navigateToStudioDocument()
236
+
237
+ // Should choose the NO_PROJECT_ID:NO_DATASET workspace because it matches the preferred URL
238
+ expect(mockSendMessage).toHaveBeenCalledWith(
239
+ 'dashboard/v1/bridge/navigate-to-resource',
240
+ expect.objectContaining({
241
+ resourceId: 'workspace3',
242
+ }),
243
+ )
244
+ })
245
+
246
+ it('warns with preferred URL info when no matching workspace is found', () => {
247
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
248
+ const preferredUrl = 'https://nonexistent.sanity.studio'
249
+
250
+ // Set up workspaces that don't match the preferred URL
251
+ mockWorkspacesByProjectIdAndDataset = {
252
+ 'project1:dataset1': [
253
+ {
254
+ ...mockWorkspace,
255
+ url: 'https://different.sanity.studio',
256
+ },
257
+ ],
258
+ }
259
+
260
+ const {result} = renderHook(() => useNavigateToStudioDocument(mockDocumentHandle, preferredUrl))
261
+
262
+ // Simulate connection
263
+ act(() => {
264
+ mockStatusCallback?.('connected')
265
+ })
266
+
267
+ result.current.navigateToStudioDocument()
268
+
269
+ expect(consoleSpy).toHaveBeenCalledWith(
270
+ `No workspace found for document with projectId: ${mockDocumentHandle.projectId} and dataset: ${mockDocumentHandle.dataset} or with preferred studio url: ${preferredUrl}`,
271
+ )
272
+ expect(mockSendMessage).not.toHaveBeenCalled()
273
+
274
+ consoleSpy.mockRestore()
275
+ })
160
276
  })
@@ -4,7 +4,10 @@ import {type DocumentHandle} from '@sanity/sdk'
4
4
  import {useCallback, useState} from 'react'
5
5
 
6
6
  import {useWindowConnection} from '../comlink/useWindowConnection'
7
- import {useStudioWorkspacesByProjectIdDataset} from './useStudioWorkspacesByProjectIdDataset'
7
+ import {
8
+ type DashboardResource,
9
+ useStudioWorkspacesByProjectIdDataset,
10
+ } from './useStudioWorkspacesByProjectIdDataset'
8
11
 
9
12
  /**
10
13
  * @public
@@ -28,6 +31,8 @@ export interface NavigateToStudioResult {
28
31
  *
29
32
  * @category Documents
30
33
  * @param documentHandle - The document handle for the document to navigate to
34
+ * @param preferredStudioUrl - The preferred studio url to navigate to if you have multiple
35
+ * studios with the same projectId and dataset
31
36
  * @returns An object containing:
32
37
  * - `navigateToStudioDocument` - Function that when called will navigate to the studio document
33
38
  * - `isConnected` - Boolean indicating if connection to Dashboard is established
@@ -49,6 +54,7 @@ export interface NavigateToStudioResult {
49
54
  */
50
55
  export function useNavigateToStudioDocument(
51
56
  documentHandle: DocumentHandle,
57
+ preferredStudioUrl?: string,
52
58
  ): NavigateToStudioResult {
53
59
  const {workspacesByProjectIdAndDataset, isConnected: workspacesConnected} =
54
60
  useStudioWorkspacesByProjectIdDataset()
@@ -62,40 +68,69 @@ export function useNavigateToStudioDocument(
62
68
  const navigateToStudioDocument = useCallback(() => {
63
69
  const {projectId, dataset} = documentHandle
64
70
 
65
- if (!workspacesConnected || status !== 'connected' || !projectId || !dataset) {
71
+ if (!workspacesConnected || status !== 'connected') {
72
+ // eslint-disable-next-line no-console
73
+ console.warn('Not connected to Dashboard')
66
74
  return
67
75
  }
68
76
 
69
- // Find the workspace for this document
70
- const workspaces = workspacesByProjectIdAndDataset[`${projectId}:${dataset}`]
71
- if (!workspaces?.length) {
77
+ if (!projectId || !dataset) {
72
78
  // eslint-disable-next-line no-console
73
- console.warn(
74
- `No workspace found for document with projectId: ${projectId} and dataset: ${dataset}`,
75
- )
79
+ console.warn('Project ID and dataset are required to navigate to a studio document')
76
80
  return
77
81
  }
78
82
 
79
- if (workspaces.length > 1) {
80
- // eslint-disable-next-line no-console
81
- console.warn('Multiple workspaces found for document', documentHandle)
82
- // eslint-disable-next-line no-console
83
- console.warn('Using the first one', workspaces[0])
83
+ let workspace: DashboardResource | undefined
84
+
85
+ if (preferredStudioUrl) {
86
+ // Get workspaces matching the projectId:dataset and any workspaces without projectId/dataset,
87
+ // in case there hasn't been a manifest loaded yet
88
+ const allWorkspaces = [
89
+ ...(workspacesByProjectIdAndDataset[`${projectId}:${dataset}`] || []),
90
+ ...(workspacesByProjectIdAndDataset['NO_PROJECT_ID:NO_DATASET'] || []),
91
+ ]
92
+ workspace = allWorkspaces.find((w) => w.url === preferredStudioUrl)
93
+ } else {
94
+ const workspaces = workspacesByProjectIdAndDataset[`${projectId}:${dataset}`]
95
+ if (workspaces?.length > 1) {
96
+ // eslint-disable-next-line no-console
97
+ console.warn(
98
+ 'Multiple workspaces found for document and no preferred studio url',
99
+ documentHandle,
100
+ )
101
+ // eslint-disable-next-line no-console
102
+ console.warn('Using the first one', workspaces[0])
103
+ }
104
+
105
+ workspace = workspaces?.[0]
84
106
  }
85
107
 
86
- const workspace = workspaces[0]
108
+ if (!workspace) {
109
+ // eslint-disable-next-line no-console
110
+ console.warn(
111
+ `No workspace found for document with projectId: ${projectId} and dataset: ${dataset}${preferredStudioUrl ? ` or with preferred studio url: ${preferredStudioUrl}` : ''}`,
112
+ )
113
+ return
114
+ }
87
115
 
88
116
  const message: Bridge.Navigation.NavigateToResourceMessage = {
89
117
  type: 'dashboard/v1/bridge/navigate-to-resource',
90
118
  data: {
91
- resourceId: workspace._ref,
119
+ resourceId: workspace.id,
92
120
  resourceType: 'studio',
93
121
  path: `/intent/edit/id=${documentHandle.documentId};type=${documentHandle.documentType}`,
94
122
  },
95
123
  }
96
124
 
97
125
  sendMessage(message.type, message.data)
98
- }, [documentHandle, workspacesConnected, status, workspacesByProjectIdAndDataset, sendMessage])
126
+ }, [
127
+ documentHandle,
128
+ workspacesConnected,
129
+ status,
130
+ workspacesByProjectIdAndDataset,
131
+ sendMessage,
132
+ preferredStudioUrl,
133
+ ])
99
134
 
100
135
  return {
101
136
  navigateToStudioDocument,
@@ -13,52 +13,47 @@ const mockWorkspaceData = {
13
13
  context: {
14
14
  availableResources: [
15
15
  {
16
+ id: 'user1-workspace1',
16
17
  projectId: 'project1',
17
- workspaces: [
18
- {
19
- name: 'workspace1',
20
- title: 'Workspace 1',
21
- basePath: '/workspace1',
22
- dataset: 'dataset1',
23
- userApplicationId: 'user1',
24
- url: 'https://test.sanity.studio',
25
- _ref: 'user1-workspace1',
26
- },
27
- {
28
- name: 'workspace2',
29
- title: 'Workspace 2',
30
- basePath: '/workspace2',
31
- dataset: 'dataset1',
32
- userApplicationId: 'user1',
33
- url: 'https://test.sanity.studio',
34
- _ref: 'user1-workspace2',
35
- },
36
- ],
18
+ dataset: 'dataset1',
19
+ name: 'workspace1',
20
+ title: 'Workspace 1',
21
+ basePath: '/workspace1',
22
+ userApplicationId: 'user1',
23
+ url: 'https://test1.sanity.studio',
24
+ type: 'studio',
37
25
  },
38
26
  {
39
- projectId: 'project2',
40
- workspaces: [
41
- {
42
- name: 'workspace3',
43
- title: 'Workspace 3',
44
- basePath: '/workspace3',
45
- dataset: 'dataset2',
46
- userApplicationId: 'user2',
47
- url: 'https://test.sanity.studio',
48
- _ref: 'user2-workspace3',
49
- },
50
- ],
27
+ id: 'user1-workspace2',
28
+ projectId: 'project1',
29
+ dataset: 'dataset1',
30
+ name: 'workspace2',
31
+ title: 'Workspace 2',
32
+ basePath: '/workspace2',
33
+ userApplicationId: 'user1',
34
+ url: 'https://test2.sanity.studio',
35
+ type: 'studio',
51
36
  },
52
37
  {
53
- // Project without workspaces
54
- projectId: 'project3',
55
- workspaces: [],
38
+ id: 'user2-workspace3',
39
+ projectId: 'project2',
40
+ dataset: 'dataset2',
41
+ name: 'workspace3',
42
+ title: 'Workspace 3',
43
+ basePath: '/workspace3',
44
+ userApplicationId: 'user2',
45
+ url: 'https://test3.sanity.studio',
46
+ type: 'studio',
56
47
  },
57
48
  ],
58
49
  },
59
50
  }
60
51
 
61
52
  describe('useStudioWorkspacesByResourceId', () => {
53
+ afterEach(() => {
54
+ vi.clearAllMocks()
55
+ })
56
+
62
57
  it('should return empty workspaces and connected=false when not connected', async () => {
63
58
  // Create a mock that captures the onStatus callback
64
59
  let capturedOnStatus: ((status: Status) => void) | undefined
@@ -106,33 +101,39 @@ describe('useStudioWorkspacesByResourceId', () => {
106
101
  expect(result.current.workspacesByProjectIdAndDataset).toEqual({
107
102
  'project1:dataset1': [
108
103
  {
104
+ id: 'user1-workspace1',
105
+ projectId: 'project1',
106
+ dataset: 'dataset1',
109
107
  name: 'workspace1',
110
108
  title: 'Workspace 1',
111
109
  basePath: '/workspace1',
112
- dataset: 'dataset1',
113
110
  userApplicationId: 'user1',
114
- url: 'https://test.sanity.studio',
115
- _ref: 'user1-workspace1',
111
+ url: 'https://test1.sanity.studio',
112
+ type: 'studio',
116
113
  },
117
114
  {
115
+ id: 'user1-workspace2',
116
+ projectId: 'project1',
117
+ dataset: 'dataset1',
118
118
  name: 'workspace2',
119
119
  title: 'Workspace 2',
120
120
  basePath: '/workspace2',
121
- dataset: 'dataset1',
122
121
  userApplicationId: 'user1',
123
- url: 'https://test.sanity.studio',
124
- _ref: 'user1-workspace2',
122
+ url: 'https://test2.sanity.studio',
123
+ type: 'studio',
125
124
  },
126
125
  ],
127
126
  'project2:dataset2': [
128
127
  {
128
+ id: 'user2-workspace3',
129
+ projectId: 'project2',
130
+ dataset: 'dataset2',
129
131
  name: 'workspace3',
130
132
  title: 'Workspace 3',
131
133
  basePath: '/workspace3',
132
- dataset: 'dataset2',
133
134
  userApplicationId: 'user2',
134
- url: 'https://test.sanity.studio',
135
- _ref: 'user2-workspace3',
135
+ url: 'https://test3.sanity.studio',
136
+ type: 'studio',
136
137
  },
137
138
  ],
138
139
  })
@@ -199,60 +200,56 @@ describe('useStudioWorkspacesByResourceId', () => {
199
200
  })
200
201
  })
201
202
 
202
- it('should handle projects without workspaces', async () => {
203
- const mockFetch = vi.fn().mockResolvedValue({
203
+ it('should filter non-studio resources and handle resources without projectId/dataset', async () => {
204
+ const mockDataWithMixedResources = {
204
205
  context: {
205
206
  availableResources: [
206
207
  {
208
+ id: 'studio1',
207
209
  projectId: 'project1',
208
- workspaces: [],
210
+ dataset: 'dataset1',
211
+ name: 'workspace1',
212
+ title: 'Workspace 1',
213
+ basePath: '/workspace1',
214
+ userApplicationId: 'user1',
215
+ url: 'https://test1.sanity.studio',
216
+ type: 'studio',
209
217
  },
210
- ],
211
- },
212
- })
213
- let capturedOnStatus: ((status: Status) => void) | undefined
214
-
215
- vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
216
- capturedOnStatus = onStatus
217
-
218
- return {
219
- fetch: mockFetch,
220
- sendMessage: vi.fn(),
221
- } as unknown as WindowConnection<Message>
222
- })
223
-
224
- const {result} = renderHook(() => useStudioWorkspacesByProjectIdDataset())
225
-
226
- // Call onStatus with 'connected' to simulate connected state
227
- if (capturedOnStatus) capturedOnStatus('connected')
228
-
229
- await waitFor(() => {
230
- expect(result.current.workspacesByProjectIdAndDataset).toEqual({})
231
- expect(result.current.error).toBeNull()
232
- expect(result.current.isConnected).toBe(true)
233
- })
234
- })
235
-
236
- it('should handle projects without projectId', async () => {
237
- const mockFetch = vi.fn().mockResolvedValue({
238
- context: {
239
- availableResources: [
240
218
  {
241
- workspaces: [
242
- {
243
- name: 'workspace1',
244
- title: 'Workspace 1',
245
- basePath: '/workspace1',
246
- dataset: 'dataset1',
247
- userApplicationId: 'user1',
248
- url: 'https://test.sanity.studio',
249
- _ref: 'user1-workspace1',
250
- },
251
- ],
219
+ id: 'non-studio',
220
+ projectId: 'project2',
221
+ dataset: 'dataset2',
222
+ name: 'non-studio',
223
+ title: 'Non Studio Resource',
224
+ basePath: '/non-studio',
225
+ userApplicationId: 'user2',
226
+ url: 'https://test2.sanity.studio',
227
+ type: 'other',
228
+ },
229
+ {
230
+ id: 'studio-no-project',
231
+ name: 'incomplete-workspace',
232
+ title: 'Incomplete Workspace',
233
+ basePath: '/incomplete',
234
+ userApplicationId: 'user3',
235
+ url: 'https://test3.sanity.studio',
236
+ type: 'studio',
237
+ },
238
+ {
239
+ id: 'studio-no-dataset',
240
+ projectId: 'project3',
241
+ name: 'no-dataset-workspace',
242
+ title: 'No Dataset Workspace',
243
+ basePath: '/no-dataset',
244
+ userApplicationId: 'user4',
245
+ url: 'https://test4.sanity.studio',
246
+ type: 'studio',
252
247
  },
253
248
  ],
254
249
  },
255
- })
250
+ }
251
+
252
+ const mockFetch = vi.fn().mockResolvedValue(mockDataWithMixedResources)
256
253
  let capturedOnStatus: ((status: Status) => void) | undefined
257
254
 
258
255
  vi.mocked(useWindowConnection).mockImplementation(({onStatus}) => {
@@ -270,7 +267,23 @@ describe('useStudioWorkspacesByResourceId', () => {
270
267
  if (capturedOnStatus) capturedOnStatus('connected')
271
268
 
272
269
  await waitFor(() => {
273
- expect(result.current.workspacesByProjectIdAndDataset).toEqual({})
270
+ // Should only include the studio resource with valid projectId and dataset
271
+ expect(result.current.workspacesByProjectIdAndDataset['project1:dataset1']).toHaveLength(1)
272
+ expect(result.current.workspacesByProjectIdAndDataset['project1:dataset1'][0].id).toBe(
273
+ 'studio1',
274
+ )
275
+
276
+ // Should not include the non-studio resource
277
+ expect(result.current.workspacesByProjectIdAndDataset['project2:dataset2']).toBeUndefined()
278
+
279
+ // Should group resources without projectId or dataset under NO_PROJECT_ID:NO_DATASET
280
+ expect(
281
+ result.current.workspacesByProjectIdAndDataset['NO_PROJECT_ID:NO_DATASET'],
282
+ ).toHaveLength(2)
283
+ expect(
284
+ result.current.workspacesByProjectIdAndDataset['NO_PROJECT_ID:NO_DATASET'].map((r) => r.id),
285
+ ).toEqual(['studio-no-project', 'studio-no-dataset'])
286
+
274
287
  expect(result.current.error).toBeNull()
275
288
  expect(result.current.isConnected).toBe(true)
276
289
  })