@sanity/sdk-react 3.0.0-rc.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_exports/dashboard.d.ts +209 -0
- package/dist/_exports/dashboard.d.ts.map +1 -0
- package/dist/_exports/dashboard.js +278 -0
- package/dist/_exports/dashboard.js.map +1 -0
- package/dist/index.d.ts +2186 -2774
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3953 -1951
- package/dist/index.js.map +1 -1
- package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js +177 -0
- package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js.map +1 -0
- package/package.json +19 -19
- package/src/_exports/dashboard.ts +11 -0
- package/src/_exports/sdk-react.ts +11 -18
- package/src/components/SDKProvider.test.tsx +5 -5
- package/src/components/SanityApp.tsx +2 -3
- package/src/components/auth/AuthBoundary.test.tsx +1 -19
- package/src/components/auth/AuthBoundary.tsx +7 -6
- package/src/components/auth/LoginError.tsx +2 -3
- package/src/context/ComlinkTokenRefresh.tsx +3 -5
- package/src/context/{WorkbenchTokenRefresh.test.tsx → DashboardTokenRefresh.test.tsx} +26 -26
- package/src/context/{WorkbenchTokenRefresh.tsx → DashboardTokenRefresh.tsx} +14 -14
- package/src/context/OrganizationResourcesProvider.test.tsx +9 -9
- package/src/context/OrganizationResourcesProvider.tsx +2 -2
- package/src/context/ResourceProvider.test.tsx +21 -0
- package/src/context/ResourceProvider.tsx +4 -1
- package/src/context/{workbenchToken.ts → dashboardToken.ts} +13 -13
- package/src/hooks/agent/agentActions.test.tsx +1 -1
- package/src/hooks/agent/agentActions.ts +1 -1
- package/src/hooks/comlink/useFrameConnection.test.tsx +3 -2
- package/src/hooks/comlink/useFrameConnection.ts +1 -1
- package/src/hooks/comlink/useWindowConnection.test.tsx +4 -3
- package/src/hooks/comlink/useWindowConnection.ts +3 -4
- package/src/hooks/comments/useCommentActions.test.tsx +150 -0
- package/src/hooks/comments/useCommentActions.ts +109 -0
- package/src/hooks/comments/useCommentList.ts +79 -0
- package/src/hooks/comments/useCommentThreads.test.tsx +107 -0
- package/src/hooks/comments/useCommentThreads.ts +73 -0
- package/src/hooks/comments/useComments.test.tsx +242 -0
- package/src/hooks/comments/useComments.ts +59 -0
- package/src/hooks/context/useSanityInstance.test.tsx +2 -54
- package/src/hooks/context/useSanityInstance.ts +2 -24
- package/src/hooks/{agent → dashboard}/useAgentResourceContext.ts +2 -2
- package/src/hooks/dashboard/useFavorite.test.tsx +101 -0
- package/src/hooks/dashboard/useFavorite.ts +34 -0
- package/src/hooks/dashboard/useFavoriteContext.ts +61 -0
- package/src/hooks/dashboard/{useDashboardNavigate.test.ts → useNavigate.test.ts} +3 -3
- package/src/hooks/dashboard/{useDashboardNavigate.ts → useNavigate.ts} +5 -5
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +2 -1
- package/src/hooks/{auth/useDashboardOrganizationId.test.tsx → dashboard/useOrganizationId.test.tsx} +4 -4
- package/src/hooks/{auth/useDashboardOrganizationId.tsx → dashboard/useOrganizationId.tsx} +2 -2
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +2 -1
- package/src/hooks/dashboard/useUpdateFavorite.test.tsx +146 -0
- package/src/hooks/dashboard/useUpdateFavorite.ts +74 -0
- package/src/hooks/dashboard/useWindowTitle.ts +1 -1
- package/src/hooks/datasets/useDatasets.test.tsx +29 -22
- package/src/hooks/datasets/useDatasets.ts +31 -53
- package/src/hooks/documents/useDocuments.ts +2 -7
- package/src/hooks/helpers/useNormalizedResourceOptions.test.tsx +0 -27
- package/src/hooks/helpers/useNormalizedResourceOptions.ts +14 -25
- package/src/hooks/paginatedDocuments/usePaginatedDocuments.ts +2 -7
- package/src/hooks/presence/usePresence.ts +14 -1
- package/src/hooks/presence/usePresenceForDocument.test.tsx +141 -0
- package/src/hooks/presence/usePresenceForDocument.ts +104 -0
- package/src/hooks/presence/useReportPresence.test.tsx +202 -0
- package/src/hooks/presence/useReportPresence.ts +170 -0
- package/src/hooks/preview/useDocumentPreview.tsx +2 -6
- package/src/hooks/projects/useProjects.ts +0 -8
- package/src/hooks/query/useQuery.ts +3 -8
- package/src/hooks/releases/useActiveReleases.ts +2 -2
- package/src/hooks/releases/useAllReleases.ts +2 -2
- package/src/hooks/users/useUser.ts +2 -8
- package/src/hooks/users/useUsers.ts +1 -2
- package/src/hooks/applications/useCreateUserApplication.test-d.ts +0 -14
- package/src/hooks/applications/useCreateUserApplication.ts +0 -11
- package/src/hooks/applications/useDeleteUserApplication.test-d.ts +0 -14
- package/src/hooks/applications/useDeleteUserApplication.ts +0 -11
- package/src/hooks/applications/useUpdateUserApplication.test-d.ts +0 -14
- package/src/hooks/applications/useUpdateUserApplication.ts +0 -11
- package/src/hooks/applications/useUserApplication.test-d.ts +0 -11
- package/src/hooks/applications/useUserApplication.ts +0 -14
- package/src/hooks/applications/useUserApplications.test-d.ts +0 -11
- package/src/hooks/applications/useUserApplications.ts +0 -14
- package/src/hooks/dashboard/useManageFavorite.test.tsx +0 -379
- package/src/hooks/dashboard/useManageFavorite.ts +0 -173
- /package/src/hooks/{agent → dashboard}/useAgentResourceContext.test.tsx +0 -0
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from './
|
|
13
|
-
import {
|
|
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('./
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
36
|
-
const
|
|
37
|
-
const
|
|
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
|
-
<
|
|
42
|
+
<DashboardTokenRefreshProvider>
|
|
43
43
|
<div>Test</div>
|
|
44
|
-
</
|
|
44
|
+
</DashboardTokenRefreshProvider>
|
|
45
45
|
</ResourceProvider>,
|
|
46
46
|
)
|
|
47
47
|
|
|
48
|
-
describe('
|
|
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
|
|
57
|
+
describe('when not in the dashboard', () => {
|
|
58
58
|
it('does not subscribe to the OS token', () => {
|
|
59
|
-
|
|
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
|
|
69
|
+
describe('when in the dashboard', () => {
|
|
70
70
|
beforeEach(() => {
|
|
71
|
-
|
|
71
|
+
mockIsDashboardEnvironment.mockReturnValue(true)
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
it('mirrors the OS token into the auth store', () => {
|
|
75
|
-
|
|
75
|
+
mockObserveDashboardToken.mockReturnValue(of('dashboard-token'))
|
|
76
76
|
|
|
77
77
|
act(() => {
|
|
78
78
|
renderProvider()
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
-
expect(mockSetAuthToken).toHaveBeenCalledWith(expect.anything(), '
|
|
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
|
-
|
|
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
|
-
<
|
|
96
|
+
<DashboardTokenRefreshProvider>
|
|
97
97
|
<div>Test</div>
|
|
98
|
-
</
|
|
98
|
+
</DashboardTokenRefreshProvider>
|
|
99
99
|
</ResourceProvider>,
|
|
100
100
|
)
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
expect(
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from './
|
|
8
|
+
isDashboardEnvironment,
|
|
9
|
+
observeDashboardToken,
|
|
10
|
+
refreshDashboardToken,
|
|
11
|
+
} from './dashboardToken'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Keeps the SDK auth token in sync with the
|
|
14
|
+
* Keeps the SDK auth token in sync with the dashboard "OS".
|
|
15
15
|
*
|
|
16
|
-
* When running as a federated remote inside 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
|
|
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$ =
|
|
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
|
-
|
|
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
|
|
51
|
+
* Provides dashboard OS token refresh. No-op outside the dashboard, where the
|
|
52
52
|
* app uses its normal auth flow.
|
|
53
|
-
* @
|
|
53
|
+
* @internal
|
|
54
54
|
*/
|
|
55
|
-
export const
|
|
56
|
-
if (
|
|
57
|
-
return <
|
|
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 {
|
|
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/
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
30
|
-
* caller uses its normal auth flow. Inside 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
|
|
39
|
-
if (!
|
|
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
|
|
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
|
|
52
|
-
* the
|
|
51
|
+
* the `auth.token` subscription in {@link observeDashboardToken}. No-op outside
|
|
52
|
+
* the dashboard.
|
|
53
53
|
*
|
|
54
54
|
* @internal
|
|
55
55
|
*/
|
|
56
|
-
export function
|
|
57
|
-
if (!
|
|
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),
|
|
@@ -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} =
|
|
17
|
+
const {getOrCreateChannel, getOrCreateController, releaseChannel} =
|
|
18
|
+
await import('@sanity/sdk/comlink')
|
|
18
19
|
|
|
19
20
|
interface TestControllerMessage {
|
|
20
21
|
type: 'TEST_MESSAGE'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {type Message, type Node} from '@sanity/comlink'
|
|
2
|
-
import {
|
|
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 `
|
|
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
|
+
})
|