@sanity/sdk-react 3.4.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 +40 -12
- package/dist/_exports/dashboard.d.ts.map +1 -1
- package/dist/_exports/dashboard.js +49 -12
- package/dist/_exports/dashboard.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{useStudioWorkspacesByProjectIdDataset-Bjwi4cLk.js → useStudioWorkspacesByProjectIdDataset-DL6_9jgU.js} +2 -2
- package/dist/{useStudioWorkspacesByProjectIdDataset-Bjwi4cLk.js.map → useStudioWorkspacesByProjectIdDataset-DL6_9jgU.js.map} +1 -1
- package/package.json +2 -2
- package/src/_exports/dashboard.ts +2 -0
- package/src/hooks/dashboard/useApplications.test.tsx +165 -7
- package/src/hooks/dashboard/useApplications.ts +112 -28
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +23 -1
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +2 -1
|
@@ -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
|
}
|
|
@@ -305,7 +305,22 @@ describe('useStudioWorkspacesByProjectIdDataset (message bus)', () => {
|
|
|
305
305
|
})
|
|
306
306
|
|
|
307
307
|
it('maps studio workspaces to resources keyed by projectId:dataset', () => {
|
|
308
|
-
|
|
308
|
+
// An installation (no `type`) shares the topic; the hook must tolerate it and map only studios.
|
|
309
|
+
const installation = {id: 'installation-1', applicationId: 'app-remote-1'}
|
|
310
|
+
// A dev-server studio is still a studio, addressed by its dev server.
|
|
311
|
+
const localStudio = {
|
|
312
|
+
...studio,
|
|
313
|
+
id: 'studio-local',
|
|
314
|
+
slug: null,
|
|
315
|
+
externalUrl: 'http://localhost:3333',
|
|
316
|
+
local: {host: 'localhost', port: 3333},
|
|
317
|
+
activeDeployment: {
|
|
318
|
+
...deployment,
|
|
319
|
+
applicationId: 'studio-local',
|
|
320
|
+
workspaces: [{...workspace, id: 'workspace-local', projectId: 'project3'}],
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
emitApplications([studio, coreApp, installation, localStudio])
|
|
309
324
|
|
|
310
325
|
const {result} = renderHookWithInstance(() => useStudioWorkspacesByProjectIdDataset())
|
|
311
326
|
|
|
@@ -327,6 +342,13 @@ describe('useStudioWorkspacesByProjectIdDataset (message bus)', () => {
|
|
|
327
342
|
expect.objectContaining({id: 'workspace-2', title: 'My Studio', basePath: ''}),
|
|
328
343
|
],
|
|
329
344
|
'project2:dataset2': [expect.objectContaining({id: 'workspace-3'})],
|
|
345
|
+
'project3:dataset1': [
|
|
346
|
+
expect.objectContaining({
|
|
347
|
+
id: 'workspace-local',
|
|
348
|
+
userApplicationId: 'studio-local',
|
|
349
|
+
url: 'http://localhost:3333',
|
|
350
|
+
}),
|
|
351
|
+
],
|
|
330
352
|
})
|
|
331
353
|
})
|
|
332
354
|
|
|
@@ -79,7 +79,8 @@ export function useStudioWorkspacesByProjectIdDataset(): StudioWorkspacesResult
|
|
|
79
79
|
// The legacy Comlink protocol models studios at the workspace level, so each workspace of a
|
|
80
80
|
// studio's active deployment becomes one resource, addressed by the studio's origin.
|
|
81
81
|
function toResources(application: DashboardApplications[number]): DashboardResource[] {
|
|
82
|
-
|
|
82
|
+
// Installations join the topic union without a `type`; only studios yield workspace resources.
|
|
83
|
+
if (!('type' in application) || application.type !== 'studio') return []
|
|
83
84
|
const url = getApplicationOrigin(application) ?? ''
|
|
84
85
|
return (application.activeDeployment?.workspaces ?? []).map((workspace) => ({
|
|
85
86
|
id: workspace.id,
|