@sanity/sdk-react 3.4.0-rc.0 → 3.5.0-next.20260922182803
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 +151 -45
- package/dist/_exports/dashboard.d.ts.map +1 -1
- package/dist/_exports/dashboard.js +267 -125
- package/dist/_exports/dashboard.js.map +1 -1
- package/dist/index.d.ts +21 -141
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -147
- package/dist/index.js.map +1 -1
- package/dist/{useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js → useStudioWorkspacesByProjectIdDataset-DL6_9jgU.js} +61 -4
- package/dist/useStudioWorkspacesByProjectIdDataset-DL6_9jgU.js.map +1 -0
- package/package.json +9 -9
- package/src/_exports/dashboard.test-d.ts +3 -0
- package/src/_exports/dashboard.ts +13 -2
- package/src/_exports/sdk-react.ts +0 -3
- package/src/components/auth/AuthBoundary.test.tsx +2 -81
- package/src/components/auth/AuthBoundary.tsx +3 -17
- package/src/components/auth/LoginCallback.test.tsx +7 -46
- package/src/components/auth/LoginCallback.tsx +4 -22
- package/src/hooks/dashboard/useAgentResourceContext.test.tsx +67 -2
- package/src/hooks/dashboard/useAgentResourceContext.ts +35 -6
- package/src/hooks/dashboard/useApplicationContext.test.tsx +95 -0
- package/src/hooks/dashboard/useApplicationContext.ts +47 -0
- package/src/hooks/dashboard/useApplications.test.tsx +165 -7
- package/src/hooks/dashboard/useApplications.ts +112 -28
- package/src/hooks/dashboard/useCapabilities.test.tsx +84 -0
- package/src/hooks/dashboard/useCapabilities.ts +27 -0
- package/src/hooks/dashboard/useNavigate.test.ts +392 -5
- package/src/hooks/dashboard/useNavigate.ts +169 -26
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +116 -9
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +110 -39
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +99 -1
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +73 -2
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +23 -1
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +2 -1
- package/dist/useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js.map +0 -1
- package/src/hooks/auth/useHandleOAuthCallback.test.tsx +0 -16
- package/src/hooks/auth/useHandleOAuthCallback.tsx +0 -49
- package/src/hooks/auth/useOAuthAuthorize.test.tsx +0 -16
- package/src/hooks/auth/useOAuthAuthorize.tsx +0 -28
- package/src/hooks/auth/useOAuthTokens.test.tsx +0 -240
- package/src/hooks/auth/useOAuthTokens.tsx +0 -95
|
@@ -1,62 +1,94 @@
|
|
|
1
|
-
import {type ApplicationBase} from '@sanity/sdk'
|
|
1
|
+
import {type ApplicationBase, type Installation, type InstallationInclude} from '@sanity/sdk'
|
|
2
2
|
import {getApplicationOrigin} from '@sanity/sdk/_internal'
|
|
3
3
|
import {type RemoteModuleRef, type ValueOf} from '@sanity/sdk/dashboard'
|
|
4
4
|
import {useMemo} from 'react'
|
|
5
5
|
|
|
6
6
|
import {useTopic} from './useTopic'
|
|
7
7
|
|
|
8
|
-
type
|
|
8
|
+
type DashboardTopicEntry = Extract<
|
|
9
9
|
NonNullable<ValueOf<'applications.list'>>,
|
|
10
10
|
{ok: true}
|
|
11
11
|
>['value'][number]
|
|
12
|
+
// The published union distinguishes an `Application` (has `type`) from an `Installation` (has not).
|
|
13
|
+
type DashboardTopicApplication = Extract<DashboardTopicEntry, {type: unknown}>
|
|
14
|
+
type DashboardTopicInstallation = Exclude<DashboardTopicEntry, {type: unknown}>
|
|
12
15
|
type DashboardApplicationInterface = NonNullable<
|
|
13
16
|
NonNullable<DashboardTopicApplication['activeDeployment']>['interfaces']
|
|
14
17
|
>[number]
|
|
15
18
|
type ViewInterface = Exclude<DashboardApplicationInterface, {type: 'worker'}>
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
21
|
+
* The shared identity fields attached to each {@link DashboardView.application} and
|
|
22
|
+
* {@link DashboardWebWorker.application}, and the input to module loading. Widens
|
|
23
|
+
* {@link ApplicationBase} so `type` also covers installations.
|
|
24
|
+
* @public
|
|
25
|
+
*/
|
|
26
|
+
export type DashboardApplicationBase = Omit<ApplicationBase, 'type'> & {
|
|
27
|
+
readonly type: ApplicationBase['type'] | 'installation'
|
|
28
|
+
/** Served by a CLI dev server rather than a deployment. */
|
|
29
|
+
readonly isLocal: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A dashboard view exposed by an application or installation.
|
|
19
34
|
* @public
|
|
20
35
|
*/
|
|
21
36
|
export type DashboardView = {
|
|
22
37
|
[Type in ViewInterface['type']]: Omit<Extract<ViewInterface, {type: Type}>, 'type'> & {
|
|
23
|
-
readonly application:
|
|
38
|
+
readonly application: DashboardApplicationBase
|
|
24
39
|
readonly module: RemoteModuleRef
|
|
25
40
|
readonly surface: Type extends 'app' ? 'window' : Type
|
|
26
41
|
}
|
|
27
42
|
}[ViewInterface['type']]
|
|
28
43
|
|
|
29
44
|
/**
|
|
30
|
-
* A web worker exposed by an application.
|
|
45
|
+
* A web worker exposed by an application or installation.
|
|
31
46
|
* @public
|
|
32
47
|
*/
|
|
33
48
|
export type DashboardWebWorker = Extract<DashboardApplicationInterface, {type: 'worker'}> & {
|
|
34
|
-
readonly application:
|
|
49
|
+
readonly application: DashboardApplicationBase
|
|
35
50
|
readonly module: RemoteModuleRef
|
|
36
51
|
}
|
|
37
52
|
|
|
38
53
|
/**
|
|
39
|
-
*
|
|
54
|
+
* An installation shaped into the shared application base, with its raw record kept under
|
|
55
|
+
* `installation` for consumers that need the full record.
|
|
40
56
|
* @public
|
|
41
57
|
*/
|
|
42
|
-
export type
|
|
43
|
-
readonly
|
|
44
|
-
readonly
|
|
58
|
+
export type DashboardInstallation = Omit<DashboardApplicationBase, 'type'> & {
|
|
59
|
+
readonly type: 'installation'
|
|
60
|
+
readonly installation: Installation<InstallationInclude>
|
|
45
61
|
}
|
|
46
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A dashboard entry — an application or an installation — in one consistent shape, with its
|
|
65
|
+
* loadable views and web workers. `type` distinguishes the kinds and `isLocal` marks dev-server
|
|
66
|
+
* applications; render a list without branching.
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
69
|
+
export type DashboardApplication = (DashboardTopicApplication | DashboardInstallation) &
|
|
70
|
+
Pick<DashboardApplicationBase, 'isLocal'> & {
|
|
71
|
+
readonly views: DashboardView[]
|
|
72
|
+
readonly webWorkers: DashboardWebWorker[]
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
// Only a federated deployment (one with a module federation manifest) exposes loadable modules.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
config,
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
// A dev server is always federated, whether or not the workbench synthesised a manifest for it.
|
|
77
|
+
const loadableInterfaces = (
|
|
78
|
+
{activeDeployment, config}: DashboardTopicApplication,
|
|
79
|
+
isLocal: boolean,
|
|
80
|
+
): readonly DashboardApplicationInterface[] =>
|
|
81
|
+
isLocal || config?.mfManifest !== undefined ? (activeDeployment?.interfaces ?? []) : []
|
|
53
82
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
// Turns an interface list into views and web workers loaded from `base`'s origin. Shared by
|
|
84
|
+
// applications and installations so the surface mapping lives in one place.
|
|
85
|
+
const loadModules = (
|
|
86
|
+
base: DashboardApplicationBase,
|
|
87
|
+
interfaces: readonly DashboardApplicationInterface[],
|
|
88
|
+
): {views: DashboardView[]; webWorkers: DashboardWebWorker[]} => {
|
|
57
89
|
// Nothing to load without interfaces or an origin to load them from.
|
|
58
|
-
const entry = interfaces.length === 0 ? null : getApplicationOrigin(
|
|
59
|
-
if (entry === null) return {
|
|
90
|
+
const entry = interfaces.length === 0 ? null : getApplicationOrigin(base)
|
|
91
|
+
if (entry === null) return {views: [], webWorkers: []}
|
|
60
92
|
|
|
61
93
|
const views: DashboardView[] = []
|
|
62
94
|
const webWorkers: DashboardWebWorker[] = []
|
|
@@ -64,12 +96,12 @@ const toApplication = (application: DashboardTopicApplication): DashboardApplica
|
|
|
64
96
|
for (const extension of interfaces) {
|
|
65
97
|
const module: RemoteModuleRef = {
|
|
66
98
|
entry,
|
|
67
|
-
moduleId: `${
|
|
99
|
+
moduleId: `${base.id}/${extension.moduleId}`,
|
|
68
100
|
version: extension.version,
|
|
69
101
|
}
|
|
70
102
|
|
|
71
103
|
if (extension.type === 'worker') {
|
|
72
|
-
webWorkers.push({...extension, application:
|
|
104
|
+
webWorkers.push({...extension, application: base, module})
|
|
73
105
|
continue
|
|
74
106
|
}
|
|
75
107
|
|
|
@@ -78,20 +110,66 @@ const toApplication = (application: DashboardTopicApplication): DashboardApplica
|
|
|
78
110
|
// cast is checked by the `DashboardView` mapping above and the surface assertions in the tests.
|
|
79
111
|
views.push({
|
|
80
112
|
...view,
|
|
81
|
-
application:
|
|
113
|
+
application: base,
|
|
82
114
|
module,
|
|
83
115
|
surface: type === 'app' ? 'window' : type,
|
|
84
116
|
} as DashboardView)
|
|
85
117
|
}
|
|
86
118
|
|
|
87
|
-
return {
|
|
119
|
+
return {views, webWorkers}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const toApplication = (application: DashboardTopicApplication): DashboardApplication => {
|
|
123
|
+
const {activeDeployment: _activeDeployment, config: _config, ...applicationBase} = application
|
|
124
|
+
const isLocal = 'local' in application
|
|
125
|
+
return {
|
|
126
|
+
...application,
|
|
127
|
+
isLocal,
|
|
128
|
+
...loadModules({...applicationBase, isLocal}, loadableInterfaces(application, isLocal)),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const toInstallation = (installation: DashboardTopicInstallation): DashboardApplication => {
|
|
133
|
+
const {application} = installation
|
|
134
|
+
// `id` is the installation record's id, not its `applicationId`, so two installs of one app never
|
|
135
|
+
// collide. `organizationId` is the publisher's: the singleton bundle is hosted under the org that
|
|
136
|
+
// published it, and `isSingleton: true` with `externalUrl: null` routes the origin through
|
|
137
|
+
// `getApplicationOrigin`'s singleton branch (`https://<slug>-apps-<publisherOrgId>.sanity.run`).
|
|
138
|
+
// The installing org stays on the raw record. The workbench always serves installations
|
|
139
|
+
// federated, so their interfaces are never gated.
|
|
140
|
+
const base: DashboardApplicationBase = {
|
|
141
|
+
id: installation.id,
|
|
142
|
+
type: 'installation',
|
|
143
|
+
isLocal: false,
|
|
144
|
+
title: application.title,
|
|
145
|
+
name: application.name,
|
|
146
|
+
reference: application.reference,
|
|
147
|
+
icon: application.icon,
|
|
148
|
+
isSingleton: true,
|
|
149
|
+
visibility: 'default',
|
|
150
|
+
slug: application.slug,
|
|
151
|
+
externalUrl: null,
|
|
152
|
+
organizationId: application.organizationId,
|
|
153
|
+
createdAt: installation.createdAt,
|
|
154
|
+
updatedAt: installation.updatedAt,
|
|
155
|
+
}
|
|
156
|
+
// The raw record rides only the entry, not every view's `application`.
|
|
157
|
+
return {
|
|
158
|
+
...base,
|
|
159
|
+
type: 'installation',
|
|
160
|
+
installation,
|
|
161
|
+
...loadModules(base, installation.interfaces ?? []),
|
|
162
|
+
}
|
|
88
163
|
}
|
|
89
164
|
|
|
90
165
|
/**
|
|
91
|
-
* Returns the applications available in the dashboard.
|
|
166
|
+
* Returns the applications and installations available in the dashboard, in one consistent shape.
|
|
92
167
|
*
|
|
93
|
-
* Suspends until the dashboard publishes its
|
|
94
|
-
*
|
|
168
|
+
* Suspends until the dashboard publishes its list; a cleared list is empty. Throws a `TopicError`
|
|
169
|
+
* to the nearest error boundary when the dashboard fails to load. Every entry exposes the same base
|
|
170
|
+
* fields plus `views` and `webWorkers`; `type` distinguishes applications (`'studio' | 'coreApp'`)
|
|
171
|
+
* from installations (`'installation'`), which also carry the raw record under `installation`.
|
|
172
|
+
* `isLocal` marks applications served by a CLI dev server.
|
|
95
173
|
*
|
|
96
174
|
* @example
|
|
97
175
|
* ```tsx
|
|
@@ -105,5 +183,11 @@ const toApplication = (application: DashboardTopicApplication): DashboardApplica
|
|
|
105
183
|
*/
|
|
106
184
|
export function useApplications(): DashboardApplication[] {
|
|
107
185
|
const applications = useTopic('applications.list')
|
|
108
|
-
return useMemo(
|
|
186
|
+
return useMemo(
|
|
187
|
+
() =>
|
|
188
|
+
applications?.map((entry) =>
|
|
189
|
+
'type' in entry ? toApplication(entry) : toInstallation(entry),
|
|
190
|
+
) ?? [],
|
|
191
|
+
[applications],
|
|
192
|
+
)
|
|
109
193
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
|
|
2
|
+
import {type CapabilityRecord, type MessageBusHost} from '@sanity/sdk/dashboard'
|
|
3
|
+
import {Suspense} from 'react'
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, expectTypeOf, it, vi} from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {act, render, renderHook, screen} from '../../../test/test-utils'
|
|
7
|
+
import {useCapabilities} from './useCapabilities'
|
|
8
|
+
|
|
9
|
+
const MESSAGE_BUS_KEY = Symbol.for('sanity.os.bus')
|
|
10
|
+
|
|
11
|
+
let host: MessageBusHost
|
|
12
|
+
|
|
13
|
+
describe('useCapabilities', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.stubGlobal('__SANITY_APP_ID__', 'app')
|
|
16
|
+
host = installMessageBus({appId: 'dashboard'})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
resetMessageBus()
|
|
21
|
+
delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
|
|
22
|
+
vi.unstubAllGlobals()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('suspends until the host publishes, then follows updates', async () => {
|
|
26
|
+
function Capabilities() {
|
|
27
|
+
const capabilities = useCapabilities()
|
|
28
|
+
expectTypeOf(capabilities).toEqualTypeOf<CapabilityRecord>()
|
|
29
|
+
return <span>{capabilities.favorites ? 'favorites' : 'no favorites'}</span>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
render(
|
|
33
|
+
<Suspense fallback="Loading">
|
|
34
|
+
<Capabilities />
|
|
35
|
+
</Suspense>,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
expect(screen.getByText('Loading')).toBeInTheDocument()
|
|
39
|
+
|
|
40
|
+
await act(async () => {
|
|
41
|
+
host.connections.subscribe((client) =>
|
|
42
|
+
client.emit('applications.capabilities', {globalUserMenu: true, history: true}),
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
expect(await screen.findByText('no favorites')).toBeInTheDocument()
|
|
46
|
+
|
|
47
|
+
act(() =>
|
|
48
|
+
host.connections.subscribe((client) =>
|
|
49
|
+
client.emit('applications.capabilities', {favorites: true}),
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
expect(screen.getByText('favorites')).toBeInTheDocument()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('treats an empty record as published, not unpublished', async () => {
|
|
56
|
+
function Capabilities() {
|
|
57
|
+
const {globalUserMenu} = useCapabilities()
|
|
58
|
+
return <span>{globalUserMenu ? 'menu' : 'no capabilities'}</span>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
render(
|
|
62
|
+
<Suspense fallback="Loading">
|
|
63
|
+
<Capabilities />
|
|
64
|
+
</Suspense>,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(screen.getByText('Loading')).toBeInTheDocument()
|
|
68
|
+
|
|
69
|
+
await act(async () => {
|
|
70
|
+
host.connections.subscribe((client) => client.emit('applications.capabilities', {}))
|
|
71
|
+
})
|
|
72
|
+
expect(await screen.findByText('no capabilities')).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('throws when used outside a dashboard application', () => {
|
|
76
|
+
resetMessageBus()
|
|
77
|
+
delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
|
|
78
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
79
|
+
|
|
80
|
+
expect(() => renderHook(() => useCapabilities())).toThrow(
|
|
81
|
+
'Cannot read topic "applications.capabilities" without an installed dashboard message bus',
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {type TopicData} from '@sanity/sdk/dashboard'
|
|
2
|
+
|
|
3
|
+
import {useTopic} from './useTopic'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the capabilities the host provides.
|
|
7
|
+
*
|
|
8
|
+
* A capability is something the host provides; an application reads this record to hide its own
|
|
9
|
+
* implementation of anything the host provides, or to skip publishing to a capability the host
|
|
10
|
+
* does not have. A missing key means the host does not provide that capability.
|
|
11
|
+
*
|
|
12
|
+
* Suspends until the host publishes the record.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* function UserMenu() {
|
|
17
|
+
* const {globalUserMenu} = useCapabilities()
|
|
18
|
+
* if (globalUserMenu) return null
|
|
19
|
+
* return <LocalUserMenu />
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
export function useCapabilities(): TopicData<'applications.capabilities'> {
|
|
26
|
+
return useTopic('applications.capabilities')
|
|
27
|
+
}
|