@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.
Files changed (85) hide show
  1. package/dist/_exports/dashboard.d.ts +209 -0
  2. package/dist/_exports/dashboard.d.ts.map +1 -0
  3. package/dist/_exports/dashboard.js +278 -0
  4. package/dist/_exports/dashboard.js.map +1 -0
  5. package/dist/index.d.ts +2186 -2774
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +3953 -1951
  8. package/dist/index.js.map +1 -1
  9. package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js +177 -0
  10. package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js.map +1 -0
  11. package/package.json +19 -19
  12. package/src/_exports/dashboard.ts +11 -0
  13. package/src/_exports/sdk-react.ts +11 -18
  14. package/src/components/SDKProvider.test.tsx +5 -5
  15. package/src/components/SanityApp.tsx +2 -3
  16. package/src/components/auth/AuthBoundary.test.tsx +1 -19
  17. package/src/components/auth/AuthBoundary.tsx +7 -6
  18. package/src/components/auth/LoginError.tsx +2 -3
  19. package/src/context/ComlinkTokenRefresh.tsx +3 -5
  20. package/src/context/{WorkbenchTokenRefresh.test.tsx → DashboardTokenRefresh.test.tsx} +26 -26
  21. package/src/context/{WorkbenchTokenRefresh.tsx → DashboardTokenRefresh.tsx} +14 -14
  22. package/src/context/OrganizationResourcesProvider.test.tsx +9 -9
  23. package/src/context/OrganizationResourcesProvider.tsx +2 -2
  24. package/src/context/ResourceProvider.test.tsx +21 -0
  25. package/src/context/ResourceProvider.tsx +4 -1
  26. package/src/context/{workbenchToken.ts → dashboardToken.ts} +13 -13
  27. package/src/hooks/agent/agentActions.test.tsx +1 -1
  28. package/src/hooks/agent/agentActions.ts +1 -1
  29. package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
  30. package/src/hooks/comlink/useFrameConnection.ts +1 -1
  31. package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
  32. package/src/hooks/comlink/useWindowConnection.ts +3 -4
  33. package/src/hooks/comments/useCommentActions.test.tsx +150 -0
  34. package/src/hooks/comments/useCommentActions.ts +109 -0
  35. package/src/hooks/comments/useCommentList.ts +79 -0
  36. package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
  37. package/src/hooks/comments/useCommentThreads.ts +73 -0
  38. package/src/hooks/comments/useComments.test.tsx +242 -0
  39. package/src/hooks/comments/useComments.ts +59 -0
  40. package/src/hooks/context/useSanityInstance.test.tsx +2 -54
  41. package/src/hooks/context/useSanityInstance.ts +2 -24
  42. package/src/hooks/{agent → dashboard}/useAgentResourceContext.ts +2 -2
  43. package/src/hooks/dashboard/useFavorite.test.tsx +101 -0
  44. package/src/hooks/dashboard/useFavorite.ts +34 -0
  45. package/src/hooks/dashboard/useFavoriteContext.ts +61 -0
  46. package/src/hooks/dashboard/{useDashboardNavigate.test.ts → useNavigate.test.ts} +3 -3
  47. package/src/hooks/dashboard/{useDashboardNavigate.ts → useNavigate.ts} +5 -5
  48. package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -1
  49. package/src/hooks/{auth/useDashboardOrganizationId.test.tsx → dashboard/useOrganizationId.test.tsx} +4 -4
  50. package/src/hooks/{auth/useDashboardOrganizationId.tsx → dashboard/useOrganizationId.tsx} +2 -2
  51. package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -1
  52. package/src/hooks/dashboard/useUpdateFavorite.test.tsx +146 -0
  53. package/src/hooks/dashboard/useUpdateFavorite.ts +74 -0
  54. package/src/hooks/dashboard/useWindowTitle.ts +1 -1
  55. package/src/hooks/datasets/useDatasets.test.tsx +29 -22
  56. package/src/hooks/datasets/useDatasets.ts +31 -53
  57. package/src/hooks/documents/useDocuments.ts +2 -7
  58. package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +0 -27
  59. package/src/hooks/helpers/useNormalizedResourceOptions.ts +14 -25
  60. package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +2 -7
  61. package/src/hooks/presence/usePresence.ts +14 -1
  62. package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
  63. package/src/hooks/presence/usePresenceForDocument.ts +104 -0
  64. package/src/hooks/presence/useReportPresence.test.tsx +202 -0
  65. package/src/hooks/presence/useReportPresence.ts +170 -0
  66. package/src/hooks/preview/useDocumentPreview.tsx +2 -6
  67. package/src/hooks/projects/useProjects.ts +0 -8
  68. package/src/hooks/query/useQuery.ts +3 -8
  69. package/src/hooks/releases/useActiveReleases.ts +2 -2
  70. package/src/hooks/releases/useAllReleases.ts +2 -2
  71. package/src/hooks/users/useUser.ts +2 -8
  72. package/src/hooks/users/useUsers.ts +1 -2
  73. package/src/hooks/applications/useCreateUserApplication.test-d.ts +0 -14
  74. package/src/hooks/applications/useCreateUserApplication.ts +0 -11
  75. package/src/hooks/applications/useDeleteUserApplication.test-d.ts +0 -14
  76. package/src/hooks/applications/useDeleteUserApplication.ts +0 -11
  77. package/src/hooks/applications/useUpdateUserApplication.test-d.ts +0 -14
  78. package/src/hooks/applications/useUpdateUserApplication.ts +0 -11
  79. package/src/hooks/applications/useUserApplication.test-d.ts +0 -11
  80. package/src/hooks/applications/useUserApplication.ts +0 -14
  81. package/src/hooks/applications/useUserApplications.test-d.ts +0 -11
  82. package/src/hooks/applications/useUserApplications.ts +0 -14
  83. package/src/hooks/dashboard/useManageFavorite.test.tsx +0 -379
  84. package/src/hooks/dashboard/useManageFavorite.ts +0 -173
  85. /package/src/hooks/{agent → dashboard}/useAgentResourceContext.test.tsx +0 -0
@@ -4,13 +4,13 @@ import {of} from 'rxjs'
4
4
  import {afterEach, beforeEach, describe, expect, it, type Mock, vi} from 'vitest'
5
5
 
6
6
  import {useAuthState} from '../hooks/auth/useAuthState'
7
- import {ResourceProvider} from './ResourceProvider'
8
7
  import {
9
- isWorkbenchEnvironment,
10
- observeWorkbenchToken,
11
- refreshWorkbenchToken,
12
- } from './workbenchToken'
13
- import {WorkbenchTokenRefreshProvider} from './WorkbenchTokenRefresh'
8
+ isDashboardEnvironment,
9
+ observeDashboardToken,
10
+ refreshDashboardToken,
11
+ } from './dashboardToken'
12
+ import {DashboardTokenRefreshProvider} from './DashboardTokenRefresh'
13
+ import {ResourceProvider} from './ResourceProvider'
14
14
 
15
15
  vi.mock('@sanity/sdk', async () => {
16
16
  const actual = await vi.importActual('@sanity/sdk')
@@ -24,28 +24,28 @@ vi.mock('../hooks/auth/useAuthState', () => ({
24
24
  useAuthState: vi.fn(),
25
25
  }))
26
26
 
27
- vi.mock('./workbenchToken', () => ({
28
- isWorkbenchEnvironment: vi.fn(() => false),
29
- observeWorkbenchToken: vi.fn(() => undefined),
30
- refreshWorkbenchToken: vi.fn(),
27
+ vi.mock('./dashboardToken', () => ({
28
+ isDashboardEnvironment: vi.fn(() => false),
29
+ observeDashboardToken: vi.fn(() => undefined),
30
+ refreshDashboardToken: vi.fn(),
31
31
  }))
32
32
 
33
33
  const mockSetAuthToken = setAuthToken as Mock
34
34
  const mockUseAuthState = useAuthState as Mock
35
- const mockIsWorkbenchEnvironment = isWorkbenchEnvironment as Mock
36
- const mockObserveWorkbenchToken = observeWorkbenchToken as Mock
37
- const mockRefreshWorkbenchToken = refreshWorkbenchToken as Mock
35
+ const mockIsDashboardEnvironment = isDashboardEnvironment as Mock
36
+ const mockObserveDashboardToken = observeDashboardToken as Mock
37
+ const mockRefreshDashboardToken = refreshDashboardToken as Mock
38
38
 
39
39
  const renderProvider = () =>
40
40
  render(
41
41
  <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
42
- <WorkbenchTokenRefreshProvider>
42
+ <DashboardTokenRefreshProvider>
43
43
  <div>Test</div>
44
- </WorkbenchTokenRefreshProvider>
44
+ </DashboardTokenRefreshProvider>
45
45
  </ResourceProvider>,
46
46
  )
47
47
 
48
- describe('WorkbenchTokenRefreshProvider', () => {
48
+ describe('DashboardTokenRefreshProvider', () => {
49
49
  beforeEach(() => {
50
50
  mockUseAuthState.mockReturnValue({type: AuthStateType.LOGGED_IN})
51
51
  })
@@ -54,9 +54,9 @@ describe('WorkbenchTokenRefreshProvider', () => {
54
54
  vi.clearAllMocks()
55
55
  })
56
56
 
57
- describe('when not in the workbench', () => {
57
+ describe('when not in the dashboard', () => {
58
58
  it('does not subscribe to the OS token', () => {
59
- mockIsWorkbenchEnvironment.mockReturnValue(false)
59
+ mockIsDashboardEnvironment.mockReturnValue(false)
60
60
 
61
61
  act(() => {
62
62
  renderProvider()
@@ -66,23 +66,23 @@ describe('WorkbenchTokenRefreshProvider', () => {
66
66
  })
67
67
  })
68
68
 
69
- describe('when in the workbench', () => {
69
+ describe('when in the dashboard', () => {
70
70
  beforeEach(() => {
71
- mockIsWorkbenchEnvironment.mockReturnValue(true)
71
+ mockIsDashboardEnvironment.mockReturnValue(true)
72
72
  })
73
73
 
74
74
  it('mirrors the OS token into the auth store', () => {
75
- mockObserveWorkbenchToken.mockReturnValue(of('workbench-token'))
75
+ mockObserveDashboardToken.mockReturnValue(of('dashboard-token'))
76
76
 
77
77
  act(() => {
78
78
  renderProvider()
79
79
  })
80
80
 
81
- expect(mockSetAuthToken).toHaveBeenCalledWith(expect.anything(), 'workbench-token')
81
+ expect(mockSetAuthToken).toHaveBeenCalledWith(expect.anything(), 'dashboard-token')
82
82
  })
83
83
 
84
84
  it('asks the OS to reissue the token on a 401', () => {
85
- mockObserveWorkbenchToken.mockReturnValue(of('workbench-token'))
85
+ mockObserveDashboardToken.mockReturnValue(of('dashboard-token'))
86
86
 
87
87
  const {rerender} = renderProvider()
88
88
 
@@ -93,14 +93,14 @@ describe('WorkbenchTokenRefreshProvider', () => {
93
93
  act(() => {
94
94
  rerender(
95
95
  <ResourceProvider projectId="test-project" dataset="test-dataset" fallback={null}>
96
- <WorkbenchTokenRefreshProvider>
96
+ <DashboardTokenRefreshProvider>
97
97
  <div>Test</div>
98
- </WorkbenchTokenRefreshProvider>
98
+ </DashboardTokenRefreshProvider>
99
99
  </ResourceProvider>,
100
100
  )
101
101
  })
102
102
 
103
- expect(mockRefreshWorkbenchToken).toHaveBeenCalledTimes(1)
103
+ expect(mockRefreshDashboardToken).toHaveBeenCalledTimes(1)
104
104
  })
105
105
  })
106
106
  })
@@ -5,28 +5,28 @@ import React, {type PropsWithChildren, useEffect, useRef} from 'react'
5
5
  import {useAuthState} from '../hooks/auth/useAuthState'
6
6
  import {useSanityInstance} from '../hooks/context/useSanityInstance'
7
7
  import {
8
- isWorkbenchEnvironment,
9
- observeWorkbenchToken,
10
- refreshWorkbenchToken,
11
- } from './workbenchToken'
8
+ isDashboardEnvironment,
9
+ observeDashboardToken,
10
+ refreshDashboardToken,
11
+ } from './dashboardToken'
12
12
 
13
13
  /**
14
- * Keeps the SDK auth token in sync with the workbench "OS".
14
+ * Keeps the SDK auth token in sync with the dashboard "OS".
15
15
  *
16
- * When running as a federated remote inside the workbench the OS owns the
16
+ * When running as a federated remote inside the dashboard the OS owns the
17
17
  * session, so we subscribe to its `auth.token` stream and mirror each value into
18
18
  * the auth store — a token logs us in, `null` logs us out, and later OS
19
19
  * sign-in/out propagates automatically. When a request is rejected with a 401
20
20
  * (the token expired), we ask the OS to reissue rather than tearing the session
21
21
  * down; the new token arrives back through the same subscription.
22
22
  */
23
- function WorkbenchTokenRefresh({children}: PropsWithChildren) {
23
+ function DashboardTokenRefresh({children}: PropsWithChildren) {
24
24
  const instance = useSanityInstance()
25
25
  const authState = useAuthState()
26
26
  const processed401ErrorRef = useRef<unknown | null>(null)
27
27
 
28
28
  useEffect(() => {
29
- const token$ = observeWorkbenchToken()
29
+ const token$ = observeDashboardToken()
30
30
  if (!token$) return undefined
31
31
  const subscription = token$.subscribe((token) => setAuthToken(instance, token))
32
32
  return () => subscription.unsubscribe()
@@ -38,7 +38,7 @@ function WorkbenchTokenRefresh({children}: PropsWithChildren) {
38
38
 
39
39
  if (has401Error && processed401ErrorRef.current !== authState.error) {
40
40
  processed401ErrorRef.current = authState.error
41
- refreshWorkbenchToken()
41
+ refreshDashboardToken()
42
42
  } else if (!has401Error) {
43
43
  processed401ErrorRef.current = null
44
44
  }
@@ -48,13 +48,13 @@ function WorkbenchTokenRefresh({children}: PropsWithChildren) {
48
48
  }
49
49
 
50
50
  /**
51
- * Provides workbench OS token refresh. No-op outside the workbench, where the
51
+ * Provides dashboard OS token refresh. No-op outside the dashboard, where the
52
52
  * app uses its normal auth flow.
53
- * @public
53
+ * @internal
54
54
  */
55
- export const WorkbenchTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
56
- if (isWorkbenchEnvironment()) {
57
- return <WorkbenchTokenRefresh>{children}</WorkbenchTokenRefresh>
55
+ export const DashboardTokenRefreshProvider: React.FC<PropsWithChildren> = ({children}) => {
56
+ if (isDashboardEnvironment()) {
57
+ return <DashboardTokenRefresh>{children}</DashboardTokenRefresh>
58
58
  }
59
59
 
60
60
  return children
@@ -3,7 +3,7 @@ import {act, render} from '@testing-library/react'
3
3
  import {type ReactNode, Suspense, useContext, useEffect} from 'react'
4
4
  import {beforeEach, describe, expect, it, vi} from 'vitest'
5
5
 
6
- import {useDashboardOrganizationId} from '../hooks/auth/useDashboardOrganizationId'
6
+ import {useOrganizationId} from '../hooks/dashboard/useOrganizationId'
7
7
  import {resolveOrgResources} from '../utils/resolveOrgResources'
8
8
  import {OrganizationResourcesProvider} from './OrganizationResourcesProvider'
9
9
  import {ResourcesContext} from './ResourcesContext'
@@ -28,8 +28,8 @@ vi.mock('../hooks/context/useSanityInstance', () => {
28
28
  return {useSanityInstance: () => instance}
29
29
  })
30
30
 
31
- vi.mock('../hooks/auth/useDashboardOrganizationId', () => ({
32
- useDashboardOrganizationId: vi.fn(),
31
+ vi.mock('../hooks/dashboard/useOrganizationId', () => ({
32
+ useOrganizationId: vi.fn(),
33
33
  }))
34
34
 
35
35
  vi.mock('../utils/resolveOrgResources', () => ({
@@ -37,14 +37,14 @@ vi.mock('../utils/resolveOrgResources', () => ({
37
37
  }))
38
38
 
39
39
  const mockResolveOrgResources = vi.mocked(resolveOrgResources)
40
- const mockUseDashboardOrganizationId = vi.mocked(useDashboardOrganizationId)
40
+ const mockUseOrganizationId = vi.mocked(useOrganizationId)
41
41
 
42
42
  describe('OrganizationResourcesProvider', () => {
43
43
  beforeEach(() => {
44
44
  vi.clearAllMocks()
45
45
  instance = createSanityInstance()
46
46
  mockResolveOrgResources.mockResolvedValue({})
47
- mockUseDashboardOrganizationId.mockReturnValue(undefined)
47
+ mockUseOrganizationId.mockReturnValue(undefined)
48
48
  })
49
49
 
50
50
  // Captures ResourcesContext via useEffect (side-effect territory, not render)
@@ -91,7 +91,7 @@ describe('OrganizationResourcesProvider', () => {
91
91
  })
92
92
 
93
93
  it('suspends children while org resources are being fetched, then renders with resolved resources', async () => {
94
- mockUseDashboardOrganizationId.mockReturnValue('org-suspend-test')
94
+ mockUseOrganizationId.mockReturnValue('org-suspend-test')
95
95
  const {promise, resolve} = promiseWithResolvers<void>()
96
96
  mockResolveOrgResources.mockReturnValue(
97
97
  promise.then(() => ({
@@ -115,7 +115,7 @@ describe('OrganizationResourcesProvider', () => {
115
115
  })
116
116
 
117
117
  it('renders without inferred entries and logs a warning when resolveOrgResources rejects', async () => {
118
- mockUseDashboardOrganizationId.mockReturnValue('org-warn-test')
118
+ mockUseOrganizationId.mockReturnValue('org-warn-test')
119
119
  const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
120
120
  mockResolveOrgResources.mockRejectedValue(new Error('fetch failed'))
121
121
 
@@ -135,7 +135,7 @@ describe('OrganizationResourcesProvider', () => {
135
135
  })
136
136
 
137
137
  it('caches Promise references across providers for the same instance', () => {
138
- mockUseDashboardOrganizationId.mockReturnValue('org-cache-test')
138
+ mockUseOrganizationId.mockReturnValue('org-cache-test')
139
139
  render(
140
140
  <Suspense fallback={null}>
141
141
  <OrganizationResourcesProvider inferMediaLibraryAndCanvas>
@@ -168,7 +168,7 @@ describe('OrganizationResourcesProvider', () => {
168
168
  })
169
169
 
170
170
  it('explicit resources take precedence over inferred ones', async () => {
171
- mockUseDashboardOrganizationId.mockReturnValue('org-override-test')
171
+ mockUseOrganizationId.mockReturnValue('org-override-test')
172
172
  mockResolveOrgResources.mockResolvedValue({
173
173
  mediaLibrary: {mediaLibraryId: 'inferred-ml'},
174
174
  canvas: {canvasId: 'inferred-canvas'},
@@ -1,8 +1,8 @@
1
1
  import {type DocumentResource, type SanityInstance} from '@sanity/sdk'
2
2
  import {type ReactElement, type ReactNode, use, useMemo} from 'react'
3
3
 
4
- import {useDashboardOrganizationId} from '../hooks/auth/useDashboardOrganizationId'
5
4
  import {useSanityInstance} from '../hooks/context/useSanityInstance'
5
+ import {useOrganizationId} from '../hooks/dashboard/useOrganizationId'
6
6
  import {resolveOrgResources} from '../utils/resolveOrgResources'
7
7
  import {ResourcesContext} from './ResourcesContext'
8
8
 
@@ -91,7 +91,7 @@ export function OrganizationResourcesProvider({
91
91
  children: ReactNode
92
92
  }): ReactElement {
93
93
  const instance = useSanityInstance()
94
- const orgId = useDashboardOrganizationId()
94
+ const orgId = useOrganizationId()
95
95
 
96
96
  if (!inferMediaLibraryAndCanvas || !orgId) {
97
97
  return (
@@ -59,6 +59,27 @@ describe('ResourceProvider', () => {
59
59
  consoleSpy.mockRestore()
60
60
  })
61
61
 
62
+ it('renders nothing during loading when fallback is null', async () => {
63
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
64
+ const {promise, resolve} = promiseWithResolvers()
65
+ function SuspendingChild(): React.ReactNode {
66
+ throw promise
67
+ }
68
+
69
+ const {container} = render(
70
+ <ResourceProvider {...testConfig} fallback={null}>
71
+ <SuspendingChild />
72
+ </ResourceProvider>,
73
+ )
74
+
75
+ expect(container).toBeEmptyDOMElement()
76
+ act(() => {
77
+ resolve()
78
+ })
79
+ await new Promise((r) => setTimeout(r, 0))
80
+ consoleSpy.mockRestore()
81
+ })
82
+
62
83
  it('creates root instance when no parent context exists', async () => {
63
84
  const {promise, resolve} = promiseWithResolvers<SanityInstance | null>()
64
85
 
@@ -130,7 +130,10 @@ export function ResourceProvider({
130
130
  }, [instance, parentInstance])
131
131
 
132
132
  return (
133
- <SanityInstanceProvider instance={instance} fallback={fallback ?? DEFAULT_FALLBACK}>
133
+ <SanityInstanceProvider
134
+ instance={instance}
135
+ fallback={fallback === undefined ? DEFAULT_FALLBACK : fallback}
136
+ >
134
137
  <ResourceContext.Provider value={effectiveResource}>
135
138
  <ProjectContext.Provider value={effectiveProjectId}>
136
139
  <PerspectiveContext.Provider value={perspective ?? parentPerspective}>
@@ -1,13 +1,13 @@
1
1
  import {from, type Observable, of} from 'rxjs'
2
2
  import {catchError, switchMap} from 'rxjs/operators'
3
3
 
4
- // The workbench host installs its shared message bus on this well-known global
4
+ // The dashboard host installs its shared message bus on this well-known global
5
5
  // symbol before it loads federated remotes. It must match the key used by
6
6
  // `@sanity/workbench` (`Symbol.for('sanity.os.bus')`).
7
7
  const OS_BUS_KEY = Symbol.for('sanity.os.bus')
8
8
 
9
9
  /**
10
- * Whether this app is running as a federated remote inside the workbench.
10
+ * Whether this app is running as a federated remote inside the dashboard.
11
11
  *
12
12
  * Federation shares the host's realm, so the installed bus is visible on
13
13
  * `globalThis`. This is `false` in a standalone app, where we must never import
@@ -18,16 +18,16 @@ const OS_BUS_KEY = Symbol.for('sanity.os.bus')
18
18
  *
19
19
  * @internal
20
20
  */
21
- export function isWorkbenchEnvironment(): boolean {
21
+ export function isDashboardEnvironment(): boolean {
22
22
  return typeof globalThis === 'object' && OS_BUS_KEY in globalThis
23
23
  }
24
24
 
25
25
  /**
26
- * Observes the session token issued by the workbench "OS", tracking the OS auth
26
+ * Observes the session token issued by the dashboard "OS", tracking the OS auth
27
27
  * state over time.
28
28
  *
29
- * Returns `undefined` when the app is not embedded in the workbench, so the
30
- * caller uses its normal auth flow. Inside the workbench, subscribes to the
29
+ * Returns `undefined` when the app is not embedded in the dashboard, so the
30
+ * caller uses its normal auth flow. Inside the dashboard, subscribes to the
31
31
  * `auth.token` state topic, emitting the current token — or `null` when the OS
32
32
  * is signed out — and re-emitting as the OS auth state changes, so sign-in/out
33
33
  * propagates instead of being captured once. Any bus error is treated as "no
@@ -35,8 +35,8 @@ export function isWorkbenchEnvironment(): boolean {
35
35
  *
36
36
  * @internal
37
37
  */
38
- export function observeWorkbenchToken(): Observable<string | null> | undefined {
39
- if (!isWorkbenchEnvironment()) return undefined
38
+ export function observeDashboardToken(): Observable<string | null> | undefined {
39
+ if (!isDashboardEnvironment()) return undefined
40
40
 
41
41
  return from(import('@sanity/workbench')).pipe(
42
42
  switchMap(({os}) => os.subscribe('auth.token')),
@@ -46,15 +46,15 @@ export function observeWorkbenchToken(): Observable<string | null> | undefined {
46
46
  }
47
47
 
48
48
  /**
49
- * Asks the workbench "OS" to reissue the session token, e.g. after its current
49
+ * Asks the dashboard "OS" to reissue the session token, e.g. after its current
50
50
  * one was rejected with a 401. Fire-and-forget: the reissued token arrives via
51
- * the `auth.token` subscription in {@link observeWorkbenchToken}. No-op outside
52
- * the workbench.
51
+ * the `auth.token` subscription in {@link observeDashboardToken}. No-op outside
52
+ * the dashboard.
53
53
  *
54
54
  * @internal
55
55
  */
56
- export function refreshWorkbenchToken(): void {
57
- if (!isWorkbenchEnvironment()) return
56
+ export function refreshDashboardToken(): void {
57
+ if (!isDashboardEnvironment()) return
58
58
 
59
59
  void import('@sanity/workbench').then(
60
60
  ({os}) => os.emit('auth.token.refresh', undefined),
@@ -12,7 +12,7 @@ import {
12
12
  useAgentTranslate,
13
13
  } from './agentActions'
14
14
 
15
- vi.mock('@sanity/sdk', async (orig) => {
15
+ vi.mock('@sanity/sdk/agent', async (orig) => {
16
16
  const actual = await orig()
17
17
  return {
18
18
  ...(actual as Record<string, any>),
@@ -11,7 +11,7 @@ import {
11
11
  type AgentTransformOptions,
12
12
  agentTranslate,
13
13
  type AgentTranslateOptions,
14
- } from '@sanity/sdk'
14
+ } from '@sanity/sdk/agent'
15
15
  import {useCallback} from 'react'
16
16
  import {firstValueFrom} from 'rxjs'
17
17
 
@@ -4,7 +4,7 @@ import {beforeEach, describe, expect, it, vi} from 'vitest'
4
4
  import {act, renderHook} from '../../../test/test-utils'
5
5
  import {useFrameConnection} from './useFrameConnection'
6
6
 
7
- vi.mock(import('@sanity/sdk'), async (importOriginal) => {
7
+ vi.mock(import('@sanity/sdk/comlink'), async (importOriginal) => {
8
8
  const actual = await importOriginal()
9
9
  return {
10
10
  ...actual,
@@ -14,7 +14,8 @@ vi.mock(import('@sanity/sdk'), async (importOriginal) => {
14
14
  }
15
15
  })
16
16
 
17
- const {getOrCreateChannel, getOrCreateController, releaseChannel} = await import('@sanity/sdk')
17
+ const {getOrCreateChannel, getOrCreateController, releaseChannel} =
18
+ await import('@sanity/sdk/comlink')
18
19
 
19
20
  interface TestControllerMessage {
20
21
  type: 'TEST_MESSAGE'
@@ -5,7 +5,7 @@ import {
5
5
  getOrCreateController,
6
6
  releaseChannel,
7
7
  type WindowMessage,
8
- } from '@sanity/sdk'
8
+ } from '@sanity/sdk/comlink'
9
9
  import {useCallback, useEffect, useRef} from 'react'
10
10
 
11
11
  import {useSanityInstance} from '../context/useSanityInstance'
@@ -1,5 +1,6 @@
1
1
  import {type Message, type Node} from '@sanity/comlink'
2
- import {getNodeState, type NodeState, type StateSource} from '@sanity/sdk'
2
+ import {type StateSource} from '@sanity/sdk'
3
+ import {getNodeState, type NodeState} from '@sanity/sdk/comlink'
3
4
  import {screen} from '@testing-library/react'
4
5
  import {Suspense} from 'react'
5
6
  import {beforeEach, describe, expect, it, vi} from 'vitest'
@@ -7,8 +8,8 @@ import {beforeEach, describe, expect, it, vi} from 'vitest'
7
8
  import {render, renderHook} from '../../../test/test-utils'
8
9
  import {useWindowConnection} from './useWindowConnection'
9
10
 
10
- vi.mock('@sanity/sdk', async () => {
11
- const actual = await vi.importActual('@sanity/sdk')
11
+ vi.mock('@sanity/sdk/comlink', async () => {
12
+ const actual = await vi.importActual('@sanity/sdk/comlink')
12
13
  return {
13
14
  ...actual,
14
15
  getNodeState: vi.fn(),
@@ -1,12 +1,11 @@
1
1
  import {type MessageData, type NodeInput} from '@sanity/comlink'
2
+ import {type SanityInstance, type StateSource} from '@sanity/sdk'
2
3
  import {
3
4
  type FrameMessage,
4
5
  getNodeState,
5
6
  type NodeState,
6
- type SanityInstance,
7
- type StateSource,
8
7
  type WindowMessage,
9
- } from '@sanity/sdk'
8
+ } from '@sanity/sdk/comlink'
10
9
  import {useCallback, useEffect, useRef} from 'react'
11
10
  import {filter, firstValueFrom} from 'rxjs'
12
11
 
@@ -67,7 +66,7 @@ const useNodeState = createStateSourceHook({
67
66
  * as well as sharing a single node between invocations if they share the same name.
68
67
  *
69
68
  * Generally not to be used directly, but to be used as a dependency of
70
- * Comlink-powered hooks like `useManageFavorite`.
69
+ * Comlink-powered hooks like `useStudioWorkspacesByProjectIdDataset`.
71
70
  */
72
71
  export function useWindowConnection<
73
72
  TWindowMessage extends WindowMessage,
@@ -0,0 +1,150 @@
1
+ import {
2
+ createComment,
3
+ removeComment,
4
+ replyToComment,
5
+ setCommentStatus,
6
+ updateComment,
7
+ } from '@sanity/sdk'
8
+ import {renderHook} from '@testing-library/react'
9
+ import {beforeEach, describe, expect, it, vi} from 'vitest'
10
+
11
+ import {ResourceProvider} from '../../context/ResourceProvider'
12
+ import {ResourcesContext} from '../../context/ResourcesContext'
13
+ import {useCommentActions} from './useCommentActions'
14
+
15
+ vi.mock('@sanity/sdk', async (importOriginal) => {
16
+ const original = await importOriginal<typeof import('@sanity/sdk')>()
17
+ return {
18
+ ...original,
19
+ createComment: vi.fn(),
20
+ replyToComment: vi.fn(),
21
+ updateComment: vi.fn(),
22
+ setCommentStatus: vi.fn(),
23
+ removeComment: vi.fn(),
24
+ }
25
+ })
26
+
27
+ const HANDLE = {documentId: 'doc-1', documentType: 'author'}
28
+
29
+ /** Creates name a field, since a comment with no path is refused. */
30
+ const CREATE = {...HANDLE, fieldPath: 'name'}
31
+
32
+ // Hoisted: an inline object here would be a new value on every render, and the
33
+ // callbacks are memoised against it.
34
+ const RESOURCES = {other: {projectId: 'p2', dataset: 'd2'}}
35
+
36
+ /** Hoisted for the same reason as `RESOURCES`. */
37
+ const RELEASE_PERSPECTIVE = {releaseName: 'summer'}
38
+
39
+ function Wrapper({children}: {children: React.ReactNode}) {
40
+ return (
41
+ <ResourceProvider projectId="p" dataset="d" fallback={null}>
42
+ <ResourcesContext.Provider value={RESOURCES}>{children}</ResourcesContext.Provider>
43
+ </ResourceProvider>
44
+ )
45
+ }
46
+
47
+ function PerspectiveWrapper({children}: {children: React.ReactNode}) {
48
+ return (
49
+ <ResourceProvider projectId="p" dataset="d" perspective={RELEASE_PERSPECTIVE} fallback={null}>
50
+ <ResourcesContext.Provider value={RESOURCES}>{children}</ResourcesContext.Provider>
51
+ </ResourceProvider>
52
+ )
53
+ }
54
+
55
+ function setup(wrapper: typeof Wrapper = Wrapper) {
56
+ return renderHook(() => useCommentActions(), {wrapper})
57
+ }
58
+
59
+ describe('useCommentActions', () => {
60
+ beforeEach(() => {
61
+ vi.resetAllMocks()
62
+ })
63
+
64
+ it('keeps the callbacks stable across renders', () => {
65
+ const {result, rerender} = setup()
66
+ const first = result.current
67
+
68
+ rerender()
69
+
70
+ expect(result.current).toBe(first)
71
+ })
72
+
73
+ it('forwards each action to the store', () => {
74
+ const {result} = setup()
75
+
76
+ result.current.createComment({...CREATE, message: null})
77
+ result.current.replyToComment({...HANDLE, parentCommentId: 'p1', message: null})
78
+ result.current.updateComment({commentId: 'c1', message: null})
79
+ result.current.setCommentStatus({commentId: 'c1', status: 'resolved'})
80
+ result.current.removeComment({commentId: 'c1'})
81
+
82
+ expect(createComment).toHaveBeenCalledOnce()
83
+ expect(replyToComment).toHaveBeenCalledOnce()
84
+ expect(updateComment).toHaveBeenCalledOnce()
85
+ expect(setCommentStatus).toHaveBeenCalledOnce()
86
+ expect(removeComment).toHaveBeenCalledOnce()
87
+ })
88
+
89
+ it('fills in the resource from context', () => {
90
+ const {result} = setup()
91
+
92
+ result.current.createComment({...CREATE, message: null})
93
+
94
+ expect(createComment).toHaveBeenCalledWith(
95
+ expect.anything(),
96
+ expect.objectContaining({resource: {projectId: 'p', dataset: 'd'}}),
97
+ )
98
+ })
99
+
100
+ it('lets a call choose a different named resource', () => {
101
+ // Resolution happens per call, so one set of callbacks serves several
102
+ // resources.
103
+ const {result} = setup()
104
+
105
+ result.current.removeComment({commentId: 'c1', resourceName: 'other'})
106
+
107
+ expect(removeComment).toHaveBeenCalledWith(
108
+ expect.anything(),
109
+ expect.objectContaining({resource: {projectId: 'p2', dataset: 'd2'}}),
110
+ )
111
+ })
112
+
113
+ it('rejects a call naming a resource that is not in context', () => {
114
+ const {result} = setup()
115
+
116
+ expect(() => result.current.removeComment({commentId: 'c1', resourceName: 'missing'})).toThrow(
117
+ /no resource named "missing"/,
118
+ )
119
+ })
120
+
121
+ it('fills in the perspective from context', () => {
122
+ // Core turns a release perspective into `target.documentVersionId`, so
123
+ // losing it here files the comment against the wrong release and nothing
124
+ // reports an error.
125
+ const {result} = setup(PerspectiveWrapper)
126
+
127
+ result.current.createComment({...CREATE, message: null})
128
+ result.current.replyToComment({...HANDLE, parentCommentId: 'p1', message: null})
129
+
130
+ expect(createComment).toHaveBeenCalledWith(
131
+ expect.anything(),
132
+ expect.objectContaining({perspective: RELEASE_PERSPECTIVE}),
133
+ )
134
+ expect(replyToComment).toHaveBeenCalledWith(
135
+ expect.anything(),
136
+ expect.objectContaining({perspective: RELEASE_PERSPECTIVE}),
137
+ )
138
+ })
139
+
140
+ it('lets a call override the perspective from context', () => {
141
+ const {result} = setup(PerspectiveWrapper)
142
+
143
+ result.current.createComment({...CREATE, message: null, perspective: 'published'})
144
+
145
+ expect(createComment).toHaveBeenCalledWith(
146
+ expect.anything(),
147
+ expect.objectContaining({perspective: 'published'}),
148
+ )
149
+ })
150
+ })