@sanity/workbench 0.1.0-alpha.2 → 0.1.0-alpha.21
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/README.md +24 -0
- package/dist/_chunks-es/index.js +39 -0
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_chunks-es/module-federation.js +59 -0
- package/dist/_chunks-es/module-federation.js.map +1 -0
- package/dist/_chunks-es/studio.js +892 -0
- package/dist/_chunks-es/studio.js.map +1 -0
- package/dist/_internal.d.ts +16 -4
- package/dist/_internal.js +34 -27
- package/dist/_internal.js.map +1 -1
- package/dist/core.d.ts +2250 -0
- package/dist/core.js +74 -0
- package/dist/core.js.map +1 -0
- package/dist/system.d.ts +2135 -0
- package/dist/system.js +887 -0
- package/dist/system.js.map +1 -0
- package/package.json +34 -6
- package/src/_exports/core.ts +1 -0
- package/src/_exports/system.ts +1 -0
- package/src/_internal/index.ts +2 -1
- package/src/_internal/render.ts +72 -43
- package/src/core/applications/application-list.ts +104 -0
- package/src/core/applications/application.ts +177 -0
- package/src/core/applications/interface.ts +126 -0
- package/src/core/canvases.ts +92 -0
- package/src/core/config.ts +34 -0
- package/src/core/env.ts +43 -0
- package/src/core/index.ts +13 -0
- package/src/core/log/index.ts +125 -0
- package/src/core/media-libraries.ts +93 -0
- package/src/core/organizations.ts +115 -0
- package/src/core/projects.ts +114 -0
- package/src/core/shared/urls.ts +129 -0
- package/src/core/user-applications/core-app.ts +148 -0
- package/src/core/user-applications/studios/index.ts +3 -0
- package/src/core/user-applications/studios/schemas.ts +128 -0
- package/src/core/user-applications/studios/studio.ts +533 -0
- package/src/core/user-applications/studios/workspace.ts +156 -0
- package/src/core/user-applications/user-application.ts +222 -0
- package/src/system/auth.machine.ts +223 -0
- package/src/system/index.ts +22 -0
- package/src/system/inspect.ts +40 -0
- package/src/system/load-federated-module.ts +53 -0
- package/src/system/module-federation.ts +116 -0
- package/src/system/remote.machine.ts +219 -0
- package/src/system/remotes.machine.ts +92 -0
- package/src/system/root.machine.ts +224 -0
- package/src/system/service.machine.ts +207 -0
- package/src/system/services.machine.ts +120 -0
- package/src/system/system-preferences.machine.ts +215 -0
- package/src/system/telemetry.machine.ts +179 -0
- package/src/_internal/render.test.ts +0 -18
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import type { SemVer } from "semver";
|
|
2
|
+
import { coerce, gt, gte, lt, rsort, valid } from "semver";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { Project } from "../../projects";
|
|
6
|
+
import { UserApplication } from "../user-application";
|
|
7
|
+
import {
|
|
8
|
+
type ClientManifest as ClientManifestType,
|
|
9
|
+
type ServerManifest as ServerManifestType,
|
|
10
|
+
type Workspace,
|
|
11
|
+
type StudioUserApplication,
|
|
12
|
+
ClientManifest as ClientManifestSchema,
|
|
13
|
+
ServerManifest as ServerManifestSchema,
|
|
14
|
+
} from "./schemas";
|
|
15
|
+
import { StudioWorkspace } from "./workspace";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validated manifest — accepts either a refined client manifest (with
|
|
19
|
+
* version ≥ 2 and `studioVersion` set for v3+) or a server/deployment
|
|
20
|
+
* manifest. Parsing fails when the input matches neither shape.
|
|
21
|
+
*/
|
|
22
|
+
const Manifest = z.union([
|
|
23
|
+
ClientManifestSchema.superRefine((data, ctx) => {
|
|
24
|
+
if (!data.version) {
|
|
25
|
+
ctx.addIssue({
|
|
26
|
+
code: "invalid_type",
|
|
27
|
+
message: "Manifest version is too old",
|
|
28
|
+
expected: "number",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (data.version < 2) {
|
|
33
|
+
ctx.addIssue({
|
|
34
|
+
code: "invalid_value",
|
|
35
|
+
message: "Manifest version is too old",
|
|
36
|
+
values: [2, 3],
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (data.version >= 3 && !data.studioVersion) {
|
|
41
|
+
ctx.addIssue({
|
|
42
|
+
code: "invalid_type",
|
|
43
|
+
message: "Manifest version 3 or higher requires a `studioVersion`",
|
|
44
|
+
expected: "string",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
ServerManifestSchema,
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const DEFAULT_WORKSPACE_DATA = {
|
|
52
|
+
name: "default",
|
|
53
|
+
title: "Default",
|
|
54
|
+
basePath: "/",
|
|
55
|
+
} as const satisfies Omit<Workspace, "projectId">;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
export class StudioApplication extends UserApplication<
|
|
61
|
+
StudioUserApplication,
|
|
62
|
+
"studio",
|
|
63
|
+
never
|
|
64
|
+
> {
|
|
65
|
+
/**
|
|
66
|
+
* Returns a list of studio workspaces based on the application manifest.
|
|
67
|
+
* If there is no manifest, or alternatively it is not valid, then we create a default workspace.
|
|
68
|
+
*/
|
|
69
|
+
readonly workspaces: readonly StudioWorkspace[] = [];
|
|
70
|
+
|
|
71
|
+
readonly project: Project<false, false>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The projects actually referenced by this studio — the application's own
|
|
75
|
+
* project plus any project used by a workspace. Preserved so the instance
|
|
76
|
+
* can be re-constructed (e.g. by dock wrappers) without having to thread
|
|
77
|
+
* the full organization project list through again.
|
|
78
|
+
*/
|
|
79
|
+
readonly projects: Project<false, false>[];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param application - The studio application to create a list of workspaces for
|
|
83
|
+
* @param projects - The projects available in the organization. It's not enough to just pass
|
|
84
|
+
* the project that associates with the application because that is the project the app is deployed in relation to.
|
|
85
|
+
* The workspaces may have different projects completely.
|
|
86
|
+
*/
|
|
87
|
+
constructor(
|
|
88
|
+
application: StudioUserApplication,
|
|
89
|
+
projects: Project<false, false>[],
|
|
90
|
+
options: {
|
|
91
|
+
isLocal?: boolean;
|
|
92
|
+
remoteApplication?: StudioApplication | null;
|
|
93
|
+
} = {},
|
|
94
|
+
) {
|
|
95
|
+
super(application, "studio", options);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Derive workspaces from the application's most authoritative manifest.
|
|
99
|
+
* `this.manifest` already prefers the deployment manifest over the
|
|
100
|
+
* client manifest; the `Manifest` schema accepts either shape and
|
|
101
|
+
* rejects malformed ones (e.g. a v3 client manifest missing
|
|
102
|
+
* `studioVersion`). A parse failure logs a warning and falls through
|
|
103
|
+
* to the default-workspace path rather than crashing.
|
|
104
|
+
*/
|
|
105
|
+
let workspaces: Workspace[] = [];
|
|
106
|
+
const manifest = this.manifest;
|
|
107
|
+
if (manifest) {
|
|
108
|
+
try {
|
|
109
|
+
workspaces = Manifest.parse(manifest).workspaces ?? [];
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn(
|
|
112
|
+
`Failed to parse manifest for application ${application.id}`,
|
|
113
|
+
error,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Filter all the workspaces that have a project the user does not have access to.
|
|
120
|
+
*/
|
|
121
|
+
const workspacesWithProjectsMap = workspaces.reduce((acc, workspace) => {
|
|
122
|
+
const project = projects.find((p) => p.id === workspace.projectId);
|
|
123
|
+
|
|
124
|
+
if (project) {
|
|
125
|
+
acc.set(workspace, project);
|
|
126
|
+
} else {
|
|
127
|
+
console.warn(
|
|
128
|
+
`Project not found for application ${application.id} and workspace ${workspace.name}. This workspace has been omitted.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return acc;
|
|
133
|
+
}, new Map<Workspace, Project<false, false>>());
|
|
134
|
+
|
|
135
|
+
const project = projects.find((p) => p.id === application.projectId);
|
|
136
|
+
|
|
137
|
+
if (!project) {
|
|
138
|
+
throw new Error(`Project not found for application ${application.id}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (workspacesWithProjectsMap.size === 0) {
|
|
142
|
+
/**
|
|
143
|
+
* If there are still no workspaces, we create a default workspace.
|
|
144
|
+
*/
|
|
145
|
+
workspacesWithProjectsMap.set(
|
|
146
|
+
{
|
|
147
|
+
...DEFAULT_WORKSPACE_DATA,
|
|
148
|
+
projectId: application.projectId,
|
|
149
|
+
} satisfies Workspace,
|
|
150
|
+
project,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.workspaces = Object.freeze(
|
|
155
|
+
Array.from(workspacesWithProjectsMap.entries()).map(([workspace, p]) => {
|
|
156
|
+
/**
|
|
157
|
+
* A workspace is considered the default if the dashboard generated it OR because
|
|
158
|
+
* the properties match that of the default workspace generated by the studio.
|
|
159
|
+
* Which is why these values will match because dashboard generates an identical
|
|
160
|
+
* workspace to the default studio one.
|
|
161
|
+
*/
|
|
162
|
+
const isDefaultWorkspace =
|
|
163
|
+
workspace.name === DEFAULT_WORKSPACE_DATA.name &&
|
|
164
|
+
workspace.basePath === DEFAULT_WORKSPACE_DATA.basePath &&
|
|
165
|
+
workspace.title === DEFAULT_WORKSPACE_DATA.title;
|
|
166
|
+
|
|
167
|
+
return this.createWorkspace(workspace, p, isDefaultWorkspace);
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
this.project = project;
|
|
172
|
+
|
|
173
|
+
// Collect only the projects actually referenced by this studio — the
|
|
174
|
+
// application's own project plus any project used by a workspace.
|
|
175
|
+
const usedProjects = new Set<Project<false, false>>([project]);
|
|
176
|
+
for (const workspace of this.workspaces) {
|
|
177
|
+
usedProjects.add(workspace.project);
|
|
178
|
+
}
|
|
179
|
+
this.projects = Array.from(usedProjects);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Factory hook called for each workspace during construction. Subclasses
|
|
184
|
+
* override this to substitute a `StudioWorkspace` subclass (e.g. a UI-aware
|
|
185
|
+
* variant in a downstream package) without re-implementing the constructor's
|
|
186
|
+
* workspace-derivation logic.
|
|
187
|
+
*/
|
|
188
|
+
protected createWorkspace(
|
|
189
|
+
workspace: Workspace,
|
|
190
|
+
project: Project<false, false>,
|
|
191
|
+
isDefaultWorkspace: boolean,
|
|
192
|
+
): StudioWorkspace {
|
|
193
|
+
return new StudioWorkspace(this, workspace, project, isDefaultWorkspace);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get href() {
|
|
197
|
+
return this.isLocal
|
|
198
|
+
? `/local/${this.id}`
|
|
199
|
+
: `/studio/${this.application.id}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
get title() {
|
|
203
|
+
// Get the title from the user application, this has the highest precedence
|
|
204
|
+
const title = this.get("title");
|
|
205
|
+
if (title) {
|
|
206
|
+
return title;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// If there are multiple workspaces and the studio is internal, use the project display name
|
|
210
|
+
if (this.workspaces.length > 1 && this.get("urlType") === "internal") {
|
|
211
|
+
return this.project.displayName;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Otherwise use the title of the first workspace
|
|
215
|
+
return this.workspaces[0].title;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
get subtitle() {
|
|
219
|
+
return new URL(this.url).hostname;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
get<TKey extends keyof StudioUserApplication>(
|
|
223
|
+
attr: TKey,
|
|
224
|
+
): StudioUserApplication[TKey] {
|
|
225
|
+
if (!(attr in this.application)) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Attribute ${attr.toString()} does not exist on studio ${this.application.id}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return this.application[attr];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolves the studio's most authoritative manifest. The lookup order is:
|
|
236
|
+
*
|
|
237
|
+
* 1. `activeDeployment.manifest` — deployment manifests are the new
|
|
238
|
+
* primitive within Sanity and are preferred whenever available.
|
|
239
|
+
* 2. `manifestData.value` — fallback to support older studios that
|
|
240
|
+
* haven't produced a deployment manifest yet.
|
|
241
|
+
* 3. `application.manifest` — last-resort legacy field.
|
|
242
|
+
*
|
|
243
|
+
* Returns `null` when no manifest is available. The return shape is a
|
|
244
|
+
* union because the deployment and client manifests are not
|
|
245
|
+
* interchangeable — consumers that depend on client-only fields (e.g.
|
|
246
|
+
* `studioVersion`, workspace `schema`) must narrow or read the raw
|
|
247
|
+
* field they need.
|
|
248
|
+
*/
|
|
249
|
+
get manifest(): ClientManifestType | ServerManifestType | null {
|
|
250
|
+
return (
|
|
251
|
+
this.application.activeDeployment?.manifest ??
|
|
252
|
+
this.application.manifestData?.value ??
|
|
253
|
+
this.application.manifest ??
|
|
254
|
+
null
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
get hasManifest(): boolean {
|
|
259
|
+
const manifest = this.manifest;
|
|
260
|
+
if (!manifest) return false;
|
|
261
|
+
|
|
262
|
+
// `manifest` is a union: server manifests encode `version` as an optional
|
|
263
|
+
// string, client manifests as a required number. The typeof check doubles
|
|
264
|
+
// as a discriminator — if it's not a number, we're looking at a server
|
|
265
|
+
// manifest whose presence alone is authoritative.
|
|
266
|
+
return typeof manifest.version !== "number" || manifest.version >= 2;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
get hasSchema(): boolean {
|
|
270
|
+
// A deployment manifest tracks a schema descriptor for every workspace,
|
|
271
|
+
// so its presence is sufficient to short-circuit.
|
|
272
|
+
if (this.application.activeDeployment?.manifest) return true;
|
|
273
|
+
|
|
274
|
+
// Otherwise inspect the client manifest. We cannot look at
|
|
275
|
+
// `this.workspaces` because it won't contain workspaces the user does
|
|
276
|
+
// not have access to. The application has a schema even if the user
|
|
277
|
+
// can't access any of the workspaces, otherwise it would be displayed
|
|
278
|
+
// as not having a manifest in the setup guide and therefore considered
|
|
279
|
+
// partially compatible.
|
|
280
|
+
const clientManifest =
|
|
281
|
+
this.application.manifestData?.value ?? this.application.manifest;
|
|
282
|
+
const workspaces = clientManifest?.workspaces ?? [];
|
|
283
|
+
|
|
284
|
+
if (workspaces.length === 0) return false;
|
|
285
|
+
return workspaces.every((w) => Boolean(w.schema));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private resolveVersion() {
|
|
289
|
+
// const growthbook = getGrowthbook()
|
|
290
|
+
|
|
291
|
+
let version: string | undefined;
|
|
292
|
+
|
|
293
|
+
if (this.get("urlType") === "internal") {
|
|
294
|
+
version = this.get("activeDeployment")?.version;
|
|
295
|
+
} else {
|
|
296
|
+
const clientManifest =
|
|
297
|
+
this.get("manifestData")?.value ?? this.get("manifest");
|
|
298
|
+
// The property 'studioVersion' exists only on external studios,
|
|
299
|
+
// starting from manifest.version 3
|
|
300
|
+
version =
|
|
301
|
+
clientManifest && "studioVersion" in clientManifest
|
|
302
|
+
? clientManifest.studioVersion
|
|
303
|
+
: undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Self-hosted studios are often not publicly accessible, which means that
|
|
308
|
+
* Brett often times cannot fetch the manifest to read the version. In this case
|
|
309
|
+
* we try to read the version from the deployment manifest or 'live-manifest', if it exists, to do
|
|
310
|
+
* everything we can to determine the version.
|
|
311
|
+
*
|
|
312
|
+
* Since the data of the live-maninfest can not be trusted, we also must validate
|
|
313
|
+
* that the version is a valid semver version before returning it. It always represents
|
|
314
|
+
* the last known version from when the studio was connected to Sanity's infrastructure,
|
|
315
|
+
* which might not be the version this studio is currently running (e.g. because the version
|
|
316
|
+
* has been downgraded).
|
|
317
|
+
*/
|
|
318
|
+
const deploymentManifest = this.get("activeDeployment")?.manifest;
|
|
319
|
+
// const liveManifest = growthbook.isOn('dashboard-use-live-manifest')
|
|
320
|
+
// ? this.get('config')?.['live-manifest']?.value
|
|
321
|
+
// : null
|
|
322
|
+
|
|
323
|
+
const serverManifest = deploymentManifest; /*?? liveManifest*/
|
|
324
|
+
const bundleVersion = serverManifest?.bundleVersion;
|
|
325
|
+
|
|
326
|
+
if (bundleVersion && valid(coerce(bundleVersion))) {
|
|
327
|
+
if (!version || (version && gt(bundleVersion, version))) {
|
|
328
|
+
version = bundleVersion;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!version || !valid(coerce(version))) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return coerce(version);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
get version() {
|
|
340
|
+
const version = this.resolveVersion();
|
|
341
|
+
return version ? version.toString() : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
get compatibilityStatus(): CompatibilityStatus {
|
|
345
|
+
return StudioApplication.resolveCompatibilityStatus(this);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Used to calculate the compatibility status of a given studio application.
|
|
350
|
+
* Optionally if you've resolved the version elsewhere provide that value to
|
|
351
|
+
* get the new compatibility status without mutating the application.
|
|
352
|
+
*/
|
|
353
|
+
static resolveCompatibilityStatus(
|
|
354
|
+
application: StudioApplication,
|
|
355
|
+
version: string | null = application.version,
|
|
356
|
+
): CompatibilityStatus {
|
|
357
|
+
if (
|
|
358
|
+
version === null ||
|
|
359
|
+
lt(version, StudioApplication.MinimumStudioVersion)
|
|
360
|
+
) {
|
|
361
|
+
return StudioApplication.CompatibilityStatuses.UNKNOWN;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (
|
|
365
|
+
!application.hasSchema ||
|
|
366
|
+
!application.hasManifest ||
|
|
367
|
+
StudioApplication.resolveIssues(application, version).length > 0
|
|
368
|
+
) {
|
|
369
|
+
return StudioApplication.CompatibilityStatuses.PARTIALLY_COMPATIBLE;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return StudioApplication.CompatibilityStatuses.FULLY_COMPATIBLE;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
get isAutoRedirecting(): boolean {
|
|
376
|
+
return StudioApplication.resolveIsAutoRedirecting(this);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Mirrors the `isRedirectable` function from Saison defined in
|
|
381
|
+
* https://github.com/sanity-io/saison/blob/83556405d23e07f6d3a71c76249c67e33fe1101f/src/utils/applications.ts
|
|
382
|
+
*
|
|
383
|
+
* Returns whether a studio application is auto-redirecting, meaning it can only be accessed in the context of
|
|
384
|
+
* Dashboard.
|
|
385
|
+
*/
|
|
386
|
+
static resolveIsAutoRedirecting(
|
|
387
|
+
application: StudioApplication,
|
|
388
|
+
versionArg = application.version,
|
|
389
|
+
) {
|
|
390
|
+
let version: string | SemVer | null = versionArg;
|
|
391
|
+
|
|
392
|
+
if (application.get("urlType") === "external" || !version) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!application.get("activeDeployment")?.isAutoUpdating) {
|
|
397
|
+
// If the studio is not auto-updating, we need to check if the version supports
|
|
398
|
+
// workspace switcher (3.92.0+) otherwise it would not be redirected.
|
|
399
|
+
return gt(version, "3.92.0");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const autoUpdatingVersion = application.get("autoUpdatingVersion");
|
|
403
|
+
|
|
404
|
+
if (autoUpdatingVersion) {
|
|
405
|
+
if (["next", "stable", "latest"].includes(autoUpdatingVersion)) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const autoUpdatingVersionPinnedVersion = coerce(autoUpdatingVersion);
|
|
410
|
+
|
|
411
|
+
if (autoUpdatingVersionPinnedVersion) {
|
|
412
|
+
version = autoUpdatingVersionPinnedVersion;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return gt(version, StudioApplication.MinimumStudioVersion);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Returns a list of issues that prevent the studio from functioning properly in the Dashboard.
|
|
421
|
+
* This static value depends on the version of the studio that comes from the `activeDeployment` property.
|
|
422
|
+
* As such, if the studio is auto-updating, this list will be incorrect & instead you should use
|
|
423
|
+
* the static method `resolveIssues` to get the correct issues by passing the resolved version.
|
|
424
|
+
*/
|
|
425
|
+
get issues(): StudioIssues {
|
|
426
|
+
return StudioApplication.resolveIssues(this);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
static resolveIssues(
|
|
430
|
+
application: StudioApplication,
|
|
431
|
+
version: string | null = application.version,
|
|
432
|
+
): StudioIssues {
|
|
433
|
+
const issues: StudioIssues = StudioApplication.Features.filter(
|
|
434
|
+
(feature) => {
|
|
435
|
+
return !application.isFeatureSupported(feature.id, version);
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
if (!application.hasManifest) {
|
|
440
|
+
issues.push({
|
|
441
|
+
id: StudioApplication.StudioIssues.ISSUE_MANIFEST,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return issues;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
protected isFeatureSupported(
|
|
449
|
+
feature: StudioDashboardIssue,
|
|
450
|
+
version = this.version,
|
|
451
|
+
) {
|
|
452
|
+
const featureVersion = StudioApplication.Features.find(
|
|
453
|
+
(_) => _.id === feature,
|
|
454
|
+
)?.version;
|
|
455
|
+
|
|
456
|
+
if (!featureVersion || !version) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return gte(version, featureVersion);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
toProtocolResource(): never {
|
|
464
|
+
throw new Error(
|
|
465
|
+
"Studio application resources cannot be converted to protocol resources",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
static CompatibilityStatuses = {
|
|
470
|
+
UNKNOWN: "unknown",
|
|
471
|
+
PARTIALLY_COMPATIBLE: "partially-compatible",
|
|
472
|
+
FULLY_COMPATIBLE: "fully-compatible",
|
|
473
|
+
} as const;
|
|
474
|
+
|
|
475
|
+
static StudioIssues = {
|
|
476
|
+
ISSUE_ACTIVITY: "ACTIVITY",
|
|
477
|
+
ISSUE_AGENT: "AGENT",
|
|
478
|
+
ISSUE_FAVORITES: "FAVORITES",
|
|
479
|
+
ISSUE_URL_SYNCING: "URL_SYNCING",
|
|
480
|
+
ISSUE_UI_ADJUSTMENT: "UI_ADJUSTMENT",
|
|
481
|
+
ISSUE_CONTENT_MAPPING: "CONTENT_MAPPING",
|
|
482
|
+
ISSUE_LOGIN: "LOGIN",
|
|
483
|
+
ISSUE_MANIFEST: "MANIFEST",
|
|
484
|
+
} as const;
|
|
485
|
+
|
|
486
|
+
static Features = [
|
|
487
|
+
{
|
|
488
|
+
id: StudioApplication.StudioIssues.ISSUE_AGENT,
|
|
489
|
+
version: "5.1.0",
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: StudioApplication.StudioIssues.ISSUE_FAVORITES,
|
|
493
|
+
version: "3.88.1",
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: StudioApplication.StudioIssues.ISSUE_ACTIVITY,
|
|
497
|
+
version: "3.88.1",
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: StudioApplication.StudioIssues.ISSUE_URL_SYNCING,
|
|
501
|
+
version: "3.75.0",
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
id: StudioApplication.StudioIssues.ISSUE_UI_ADJUSTMENT,
|
|
505
|
+
version: "3.78.1",
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: StudioApplication.StudioIssues.ISSUE_CONTENT_MAPPING,
|
|
509
|
+
version: "3.68.0",
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: StudioApplication.StudioIssues.ISSUE_LOGIN,
|
|
513
|
+
version: "2.28.0",
|
|
514
|
+
},
|
|
515
|
+
] satisfies StudioIssues;
|
|
516
|
+
|
|
517
|
+
static MinimumStudioVersion = "2.28.0" as const;
|
|
518
|
+
|
|
519
|
+
static MinimumStudioVersionWithNoIssues = rsort(
|
|
520
|
+
StudioApplication.Features.map((feature) => feature.version),
|
|
521
|
+
).at(0);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
type CompatibilityStatus =
|
|
525
|
+
(typeof StudioApplication.CompatibilityStatuses)[keyof typeof StudioApplication.CompatibilityStatuses];
|
|
526
|
+
|
|
527
|
+
type StudioDashboardIssue =
|
|
528
|
+
(typeof StudioApplication.StudioIssues)[keyof typeof StudioApplication.StudioIssues];
|
|
529
|
+
|
|
530
|
+
type StudioIssues = Array<{
|
|
531
|
+
id: StudioDashboardIssue;
|
|
532
|
+
version?: string;
|
|
533
|
+
}>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { StudioResource as ProtocolStudioResource } from "@sanity/message-protocol";
|
|
2
|
+
|
|
3
|
+
import { AbstractApplication } from "../../applications/application";
|
|
4
|
+
import type { Project } from "../../projects";
|
|
5
|
+
import { joinUrlPaths, normalizePath } from "../../shared/urls";
|
|
6
|
+
import type { UserApplicationId } from "../user-application";
|
|
7
|
+
import type { Workspace } from "./schemas";
|
|
8
|
+
import type { StudioApplication } from "./studio";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export class StudioWorkspace extends AbstractApplication<
|
|
14
|
+
"workspace",
|
|
15
|
+
ProtocolStudioResource
|
|
16
|
+
> {
|
|
17
|
+
/**
|
|
18
|
+
* Workspaces always belong to a studio application.
|
|
19
|
+
* They do not exist on their own & therefore can access
|
|
20
|
+
* information about the studio they're in via this property.
|
|
21
|
+
*/
|
|
22
|
+
private readonly studioApplication: StudioApplication;
|
|
23
|
+
private readonly workspace: Workspace;
|
|
24
|
+
readonly project: Project<false, false>;
|
|
25
|
+
private readonly isDefaultWorkspace: boolean;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
studioApplication: StudioApplication,
|
|
29
|
+
workspace: Workspace,
|
|
30
|
+
project: Project<false, false>,
|
|
31
|
+
isDefaultWorkspace: boolean,
|
|
32
|
+
) {
|
|
33
|
+
super("workspace");
|
|
34
|
+
|
|
35
|
+
this.studioApplication = studioApplication;
|
|
36
|
+
this.workspace = workspace;
|
|
37
|
+
this.project = project;
|
|
38
|
+
this.isDefaultWorkspace = isDefaultWorkspace;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The studio application that this workspace belongs to.
|
|
43
|
+
*/
|
|
44
|
+
get studio() {
|
|
45
|
+
return this.studioApplication;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get id() {
|
|
49
|
+
return StudioWorkspace.makeId(this.studio.id, this.workspace.name);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get href() {
|
|
53
|
+
return joinUrlPaths(this.studio.href, this.workspace.name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get title() {
|
|
57
|
+
/**
|
|
58
|
+
* If there's no manifest we will have created a single workspace for the application.
|
|
59
|
+
* In this circumstance we won't have a meaningful title, so instead we use the hostname of the appHost.
|
|
60
|
+
*/
|
|
61
|
+
if (this.isDefaultWorkspace) {
|
|
62
|
+
return this.project.displayName;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return this.workspace.title;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get subtitle() {
|
|
69
|
+
if (this.isDefaultWorkspace) {
|
|
70
|
+
const isValidAppHost = URL.canParse(this.studio.get("appHost"));
|
|
71
|
+
const url = isValidAppHost
|
|
72
|
+
? new URL(this.studio.get("appHost"))
|
|
73
|
+
: this.studio.url;
|
|
74
|
+
|
|
75
|
+
return url.hostname;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return this.get("subtitle");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
get<TKey extends Exclude<keyof Workspace, "id" | "icon" | "title">>(
|
|
82
|
+
attr: TKey,
|
|
83
|
+
): Workspace[TKey] {
|
|
84
|
+
return this.workspace[attr];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get isFederated() {
|
|
88
|
+
return this.studio.isFederated;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* With comlink, studio-applications were not considered applications at all, only workspaces. This is partially why
|
|
93
|
+
* we create default workspaces when the application has no manifest or the manifest has no workspaces.
|
|
94
|
+
*
|
|
95
|
+
* Thereby, to create it we depend on a lot of information from the parent application even if it's duplicated across
|
|
96
|
+
* different workspaces.
|
|
97
|
+
*/
|
|
98
|
+
toProtocolResource(): ProtocolStudioResource {
|
|
99
|
+
return {
|
|
100
|
+
...this.workspace,
|
|
101
|
+
type: "studio",
|
|
102
|
+
userApplicationId: this.studio.id,
|
|
103
|
+
// The protocol still types `size`/`isAutoUpdating` as non-nullable,
|
|
104
|
+
// but the user-applications API reports both as nullable.
|
|
105
|
+
activeDeployment: this.studio.get(
|
|
106
|
+
"activeDeployment",
|
|
107
|
+
) as ProtocolStudioResource["activeDeployment"],
|
|
108
|
+
autoUpdatingVersion: this.studio.get("autoUpdatingVersion"),
|
|
109
|
+
dashboardStatus: this.studio.get("dashboardStatus"),
|
|
110
|
+
url: this.studio.url.toString(),
|
|
111
|
+
href: this.href,
|
|
112
|
+
id: this.id,
|
|
113
|
+
hasManifest: true,
|
|
114
|
+
hasSchema: Boolean("schema" in this.workspace && this.workspace.schema),
|
|
115
|
+
// The protocol resource requires the client-shaped manifest (workspaces
|
|
116
|
+
// with `schema`). Read those sources directly rather than the unified
|
|
117
|
+
// `manifest` getter, which may return a `ServerManifest` for studios
|
|
118
|
+
// with a deployment manifest.
|
|
119
|
+
manifest: (this.studio.get("manifestData")?.value ??
|
|
120
|
+
this.studio.get("manifest") ??
|
|
121
|
+
null) as ProtocolStudioResource["manifest"],
|
|
122
|
+
updatedAt: this.studio.get("updatedAt"),
|
|
123
|
+
version: this.studio.get("activeDeployment")?.version,
|
|
124
|
+
urlType: this.studio.get("urlType"),
|
|
125
|
+
config: this.studio.get("config"),
|
|
126
|
+
} satisfies ProtocolStudioResource;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @returns the URL to the workspace of a studio application.
|
|
131
|
+
*/
|
|
132
|
+
get url(): URL {
|
|
133
|
+
const studioUrl = new URL(this.studio.url);
|
|
134
|
+
const normalizedUrlPath = normalizePath(studioUrl.pathname);
|
|
135
|
+
|
|
136
|
+
let finalBasePath = normalizePath(this.get("basePath"));
|
|
137
|
+
|
|
138
|
+
// the appHost may already contain the basepath for externally hosted studios
|
|
139
|
+
// or embedded studios, so we deduplicate the segments
|
|
140
|
+
if (finalBasePath.startsWith(normalizedUrlPath)) {
|
|
141
|
+
finalBasePath = finalBasePath.slice(normalizedUrlPath.length);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
studioUrl.pathname = joinUrlPaths(normalizedUrlPath, finalBasePath);
|
|
145
|
+
|
|
146
|
+
return studioUrl;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static makeId(applicationId: UserApplicationId, workspaceName: string) {
|
|
150
|
+
return `${applicationId}-${workspaceName}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static splitId(id: string): [string, string] {
|
|
154
|
+
return id.split(new RegExp(/-(.*)/)).slice(0, 2) as [string, string];
|
|
155
|
+
}
|
|
156
|
+
}
|